re-add next-auth 4

This commit is contained in:
Balázs Orbán
2023-05-04 21:41:36 +02:00
parent 90b01c4613
commit bf89d9fabb
197 changed files with 13260 additions and 3 deletions

10
.gitignore vendored
View File

@@ -26,7 +26,15 @@ dist
# Generated files
.docusaurus
.cache-loader
packages/next-auth/providers
packages/next-auth/src/providers/oauth-types.ts
packages/next-auth/client
packages/next-auth/css
packages/next-auth/utils
packages/next-auth/core
packages/next-auth/jwt
packages/next-auth/react
packages/next-auth/next
packages/*/*.js
packages/*/*.d.ts
packages/*/*.d.ts.map

View File

@@ -40,6 +40,9 @@ packages/core/src/lib/pages/styles.ts
packages/frameworks-sveltekit/package
packages/frameworks-sveltekit/vite.config.{js,ts}.timestamp-*
# next-auth
packages/next-auth/src/providers/oauth-types.ts
packages/next-auth/css/index.css
# Adapters
.branches

View File

@@ -1,7 +1,7 @@
{
"files.exclude": {
"packages/core/{lib,providers,*.js,*.d.ts*}": true,
"packages/next-auth/{lib,*.js,*.d.ts*}": true,
"packages/core/{lib,providers,*.js,*.d.ts,*.d.ts.map}": true,
"packages/next-auth/{client,core,css,jwt,next,providers,react,utils,*.js,*.d.ts}": true
},
"typescript.tsdk": "node_modules/typescript/lib",
"openInGitHub.remote.branch": "main"

View File

@@ -0,0 +1 @@
//registry.npmjs.org/:_authToken=${NPM_TOKEN}

View File

@@ -0,0 +1,254 @@
<p align="center">
<br/>
<a href="https://next-auth.js.org" target="_blank"><img width="150px" src="https://next-auth.js.org/img/logo/logo-sm.png" /></a>
<h3 align="center">NextAuth.js</h3>
<p align="center">Authentication for Next.js</p>
<p align="center">
Open Source. Full Stack. Own Your Data.
</p>
<p align="center" style="align: center;">
<a href="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml?query=workflow%3ARelease">
<img src="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml/badge.svg" alt="Release" />
</a>
<a href="https://packagephobia.com/result?p=next-auth">
<img src="https://packagephobia.com/badge?p=next-auth" alt="Bundle Size"/>
</a>
<a href="https://www.npmtrends.com/next-auth">
<img src="https://img.shields.io/npm/dm/next-auth" alt="Downloads" />
</a>
<a href="https://github.com/nextauthjs/next-auth/stargazers">
<img src="https://img.shields.io/github/stars/nextauthjs/next-auth" alt="Github Stars" />
</a>
<a href="https://www.npmjs.com/package/next-auth">
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth?label=latest" alt="Github Stable Release" />
</a>
</p>
</p>
## Overview
NextAuth.js is a complete open source authentication solution for [Next.js](http://nextjs.org/) applications.
It is designed from the ground up to support Next.js and Serverless.
This is a monorepo containing the following packages / projects:
1. The primary `next-auth` package
2. A development test application
3. All `@next-auth/*-adapter` packages
4. The documentation site
## Getting Started
```
npm install next-auth
```
The easiest way to continue getting started, is to follow the [getting started](https://next-auth.js.org/getting-started/example) section in our docs.
We also have a section of [tutorials](https://next-auth.js.org/tutorials) for those looking for more specific examples.
See [next-auth.js.org](https://next-auth.js.org) for more information and documentation.
## Features
### Flexible and easy to use
- Designed to work with any OAuth service, it supports OAuth 1.0, 1.0A and 2.0
- Built-in support for [many popular sign-in services](https://next-auth.js.org/providers)
- Supports email / passwordless authentication
- Supports stateless authentication with any backend (Active Directory, LDAP, etc)
- Supports both JSON Web Tokens and database sessions
- Designed for Serverless but runs anywhere (AWS Lambda, Docker, Heroku, etc…)
### Own your own data
NextAuth.js can be used with or without a database.
- An open source solution that allows you to keep control of your data
- Supports Bring Your Own Database (BYOD) and can be used with any database
- Built-in support for [MySQL, MariaDB, Postgres, Microsoft SQL Server, MongoDB and SQLite](https://next-auth.js.org/configuration/databases)
- Works great with databases from popular hosting providers
- Can also be used _without a database_ (e.g. OAuth + JWT)
### Secure by default
- Promotes the use of passwordless sign-in mechanisms
- Designed to be secure by default and encourage best practices for safeguarding user data
- Uses Cross-Site Request Forgery (CSRF) Tokens on POST routes (sign in, sign out)
- Default cookie policy aims for the most restrictive policy appropriate for each cookie
- When JSON Web Tokens are enabled, they are encrypted by default (JWE) with A256GCM
- Auto-generates symmetric signing and encryption keys for developer convenience
- Features tab/window syncing and session polling to support short lived sessions
- Attempts to implement the latest guidance published by [Open Web Application Security Project](https://owasp.org)
Advanced options allow you to define your own routines to handle controlling what accounts are allowed to sign in, for encoding and decoding JSON Web Tokens and to set custom cookie security policies and session properties, so you can control who is able to sign in and how often sessions have to be re-validated.
### TypeScript
NextAuth.js comes with built-in types. For more information and usage, check out
the [TypeScript section](https://next-auth.js.org/getting-started/typescript) in the documentation.
## Example
### Add API Route
```javascript
// pages/api/auth/[...nextauth].js
import NextAuth from "next-auth"
import AppleProvider from "next-auth/providers/apple"
import GoogleProvider from "next-auth/providers/google"
import EmailProvider from "next-auth/providers/email"
export default NextAuth({
secret: process.env.SECRET,
providers: [
// OAuth authentication providers
AppleProvider({
clientId: process.env.APPLE_ID,
clientSecret: process.env.APPLE_SECRET,
}),
GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
// Sign in with passwordless email link
EmailProvider({
server: process.env.MAIL_SERVER,
from: "<no-reply@example.com>",
}),
],
})
```
### Add React Hook
The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
```javascript
import { useSession, signIn, signOut } from "next-auth/react"
export default function Component() {
const { data: session } = useSession()
if (session) {
return (
<>
Signed in as {session.user.email} <br />
<button onClick={() => signOut()}>Sign out</button>
</>
)
}
return (
<>
Not signed in <br />
<button onClick={() => signIn()}>Sign in</button>
</>
)
}
```
### Share/configure session state
Use the `<SessionProvider>` to allow instances of `useSession()` to share the session object across components. It also takes care of keeping the session updated and synced between tabs/windows.
```jsx title="pages/_app.js"
import { SessionProvider } from "next-auth/react"
export default function App({
Component,
pageProps: { session, ...pageProps },
}) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
)
}
```
## Security
If you think you have found a vulnerability (or not sure) in NextAuth.js or any of the related packages (i.e. Adapters), we ask you to have a read of our [Security Policy](https://github.com/nextauthjs/next-auth/blob/main/SECURITY.md) to reach out responsibly. Please do not open Pull Requests/Issues/Discussions before consulting with us.
## Acknowledgments
[NextAuth.js is made possible thanks to all of its contributors.](https://next-auth.js.org/contributors)
<a href="https://github.com/nextauthjs/next-auth/graphs/contributors">
<img width="500px" src="https://contrib.rocks/image?repo=nextauthjs/next-auth" />
</a>
<div>
<a href="https://vercel.com?utm_source=nextauthjs&utm_campaign=oss"></a>
</div>
### Support
We're happy to announce we've recently created an [OpenCollective](https://opencollective.com/nextauth) for individuals and companies looking to contribute financially to the project!
<!--sponsors start-->
<table>
<tbody>
<tr>
<td align="center" valign="top">
<a href="https://vercel.com" target="_blank">
<img width="128px" src="https://avatars.githubusercontent.com/u/14985020?v=4" alt="Vercel Logo" />
</a><br />
<div>Vercel</div><br />
<sub>🥉 Bronze Financial Sponsor <br /> ☁️ Infrastructure Support</sub>
</td>
<td align="center" valign="top">
<a href="https://prisma.io" target="_blank">
<img width="128px" src="https://avatars.githubusercontent.com/u/17219288?v=4" alt="Prisma Logo" />
</a><br />
<div>Prisma</div><br />
<sub>🥉 Bronze Financial Sponsor</sub>
</td>
<td align="center" valign="top">
<a href="https://clerk.com" target="_blank">
<img width="128px" src="https://avatars.githubusercontent.com/u/49538330?s=200&v=4" alt="Clerk Logo" />
</a><br />
<div>Clerk</div><br />
<sub>🥉 Bronze Financial Sponsor</sub>
</td>
<td align="center" valign="top">
<a href="https://lowdefy.com" target="_blank">
<img width="128px" src="https://avatars.githubusercontent.com/u/47087496?s=200&v=4" alt="Lowdefy Logo" />
</a><br />
<div>Lowdefy</div><br />
<sub>🥉 Bronze Financial Sponsor</sub>
</td>
<td align="center" valign="top">
<a href="https://workos.com" target="_blank">
<img width="128px" src="https://avatars.githubusercontent.com/u/47638084?s=200&v=4" alt="WorkOS Logo" />
</a><br />
<div>WorkOS</div><br />
<sub>🥉 Bronze Financial Sponsor</sub>
</td>
<td align="center" valign="top">
<a href="https://checklyhq.com" target="_blank">
<img width="128px" src="https://avatars.githubusercontent.com/u/25982255?v=4" alt="Checkly Logo" />
</a><br />
<div>Checkly</div><br />
<sub>☁️ Infrastructure Support</sub>
</td>
<td align="center" valign="top">
<a href="https://superblog.ai/" target="_blank">
<img width="128px" src="https://d33wubrfki0l68.cloudfront.net/cdc4a3833bd878933fcc131655878dbf226ac1c5/10cd6/images/logo_bolt_small.png" alt="superblog Logo" />
</a><br />
<div>superblog</div><br />
<sub>☁️ Infrastructure Support</sub>
</td>
</tr><tr></tr>
</tbody>
</table>
<br />
<!--sponsors end-->
## Contributing
We're open to all community contributions! If you'd like to contribute in any way, please first read
our [Contributing Guide](https://github.com/nextauthjs/.github/blob/main/CONTRIBUTING.md).
## License
ISC

View File

@@ -0,0 +1,62 @@
// @ts-check
// We aim to have the same support as Next.js
// https://nextjs.org/docs/getting-started#system-requirements
// https://nextjs.org/docs/basic-features/supported-browsers-features
/** @type {import("@babel/core").ConfigFunction} */
module.exports = (api) => {
const isTest = api.env("test")
if (isTest) {
return {
presets: [
"@babel/preset-env",
["@babel/preset-react", { runtime: "automatic" }],
["@babel/preset-typescript", { isTSX: true, allExtensions: true }],
],
}
}
return {
presets: [
["@babel/preset-env", { targets: { node: 12 } }],
"@babel/preset-typescript",
],
plugins: [
"@babel/plugin-proposal-optional-catch-binding",
"@babel/plugin-transform-runtime",
],
ignore: [
"../src/**/__tests__/**",
"../src/adapters.ts",
"../src/providers/oauth-types.ts",
],
comments: false,
overrides: [
{
test: [
"../src/react/index.tsx",
"../src/utils/logger.ts",
"../src/core/errors.ts",
"../src/client/**",
],
presets: [
["@babel/preset-env", { targets: { ie: 11 } }],
["@babel/preset-react", { runtime: "automatic" }],
],
},
{
test: ["../src/core/pages/*.tsx"],
presets: ["preact"],
plugins: [
[
"jsx-pragmatic",
{
module: "preact",
export: "h",
import: "h",
},
],
],
},
],
}
}

View File

@@ -0,0 +1,18 @@
const path = require("path")
const fs = require("fs")
const providersPath = path.join(process.cwd(), "src/providers")
const files = fs.readdirSync(providersPath, "utf8")
const providers = files.map((file) => {
const strippedProviderName = file.substring(0, file.indexOf("."))
return `"${strippedProviderName}"`
})
const result = `
// THIS FILE IS AUTOGENERATED. DO NOT EDIT.
export type OAuthProviderType =
| ${providers.join("\n | ")}`
fs.writeFileSync(path.join(providersPath, "oauth-types.ts"), result)

View File

@@ -0,0 +1,3 @@
import "regenerator-runtime/runtime"
import "@testing-library/jest-dom"
import "whatwg-fetch"

View File

@@ -0,0 +1,43 @@
const swcConfig = require("./swc.config")
/** @type {import('jest').Config} */
module.exports = {
projects: [
{
displayName: "core",
testMatch: ["<rootDir>/tests/**/*.test.ts"],
rootDir: ".",
transform: {
"\\.(js|jsx|ts|tsx)$": ["@swc/jest", swcConfig],
},
coveragePathIgnorePatterns: ["tests"],
testEnvironment: "@edge-runtime/jest-environment",
transformIgnorePatterns: ["node_modules/(?!uuid)/"],
/** @type {import("@edge-runtime/vm").EdgeVMOptions} */
testEnvironmentOptions: {
codeGeneration: {
strings: true,
},
},
},
{
displayName: "client",
testMatch: ["<rootDir>/src/client/**/*.test.js"],
setupFilesAfterEnv: ["./config/jest-setup.js"],
rootDir: ".",
transform: {
"\\.(js|jsx|ts|tsx)$": ["@swc/jest", swcConfig],
},
testEnvironment: "jsdom",
coveragePathIgnorePatterns: ["__tests__"],
},
],
watchPlugins: [
"jest-watch-typeahead/filename",
"jest-watch-typeahead/testname",
],
collectCoverage: true,
coverageDirectory: "../coverage",
coverageReporters: ["html", "text-summary"],
collectCoverageFrom: ["src/**/*.(js|jsx|ts|tsx)"],
}

View File

@@ -0,0 +1,7 @@
module.exports = {
plugins: [
require('autoprefixer'),
require('postcss-nested'),
require('cssnano')({ preset: 'default' })
]
}

View File

@@ -0,0 +1,18 @@
/** @type {import("@swc/core").Config} */
module.exports = {
jsc: {
parser: {
syntax: "typescript",
tsx: true,
},
transform: {
react: {
runtime: "automatic",
pragma: "React.createElement",
pragmaFrag: "React.Fragment",
throwIfNamespace: true,
useBuiltins: true,
},
},
},
}

View File

@@ -0,0 +1,17 @@
// Serverless target in Next.js does not work if you try to read in files at runtime
// that are not JavaScript or JSON (e.g. CSS files).
// https://github.com/nextauthjs/next-auth/issues/281
//
// To work around this issue, this script is a manual step that wraps CSS in a
// JavaScript file that has the compiled CSS embedded in it, and exports only
// a function that returns the CSS as a string.
const fs = require("fs")
const path = require("path")
const pathToCss = path.join(__dirname, "../css/index.css")
const css = fs.readFileSync(pathToCss, "utf8")
const cssWithEscapedQuotes = css.replace(/"/gm, '\\"')
const js = `module.exports = function() { return "${cssWithEscapedQuotes}" }`
const pathToCssJs = path.join(__dirname, "../css/index.js")
fs.writeFileSync(pathToCssJs, js)

View File

@@ -0,0 +1,129 @@
{
"name": "next-auth",
"version": "4.22.1",
"description": "Authentication for Next.js",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth.git",
"author": "Iain Collins <me@iaincollins.com>",
"contributors": [
"Balázs Orbán <info@balazsorban.com>",
"Nico Domino <yo@ndo.dev>",
"Lluis Agusti <hi@llu.lu>",
"Thang Huu Vu <thvu@hey.com>"
],
"main": "index.js",
"module": "index.js",
"types": "index.d.ts",
"keywords": [
"react",
"nodejs",
"oauth",
"jwt",
"oauth2",
"authentication",
"nextjs",
"csrf",
"oidc",
"nextauth"
],
"exports": {
".": "./index.js",
"./jwt": "./jwt/index.js",
"./react": "./react/index.js",
"./core": "./core/index.js",
"./next": "./next/index.js",
"./middleware": "./middleware.js",
"./client/_utils": "./client/_utils.js",
"./providers/*": "./providers/*.js"
},
"scripts": {
"build": "pnpm clean && pnpm build:js && pnpm build:css",
"build:js": "pnpm clean && pnpm generate-providers && pnpm tsc --project tsconfig.json && babel --config-file ./config/babel.config.js src --out-dir . --extensions \".tsx,.ts,.js,.jsx\"",
"clean": "rm -rf coverage client css utils providers core jwt react next index.d.ts index.js adapters.d.ts middleware.d.ts middleware.js",
"build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir . && node config/wrap-css.js",
"dev": "pnpm clean && pnpm generate-providers && concurrently \"pnpm watch:css\" \"pnpm watch:ts\"",
"watch:ts": "pnpm tsc --project tsconfig.dev.json",
"watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir .",
"test": "jest --config ./config/jest.config.js",
"prepublishOnly": "pnpm build",
"generate-providers": "node ./config/generate-providers.js",
"lint": "eslint src config tests"
},
"files": [
"client",
"core",
"css",
"jwt",
"lib",
"next",
"providers",
"react",
"src",
"utils",
"*.d.ts*",
"*.js"
],
"license": "ISC",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@panva/hkdf": "^1.0.2",
"cookie": "^0.5.0",
"jose": "^4.11.4",
"oauth": "^0.9.15",
"openid-client": "^5.4.0",
"preact": "^10.6.3",
"preact-render-to-string": "^5.1.19",
"uuid": "^8.3.2"
},
"peerDependencies": {
"next": "^12.2.5 || ^13",
"nodemailer": "^6.6.5",
"react": "^17.0.2 || ^18",
"react-dom": "^17.0.2 || ^18"
},
"peerDependenciesMeta": {
"nodemailer": {
"optional": true
}
},
"devDependencies": {
"@babel/cli": "^7.17.10",
"@babel/core": "^7.18.2",
"@babel/plugin-proposal-optional-catch-binding": "^7.16.7",
"@babel/plugin-transform-runtime": "^7.18.2",
"@babel/preset-env": "^7.18.2",
"@babel/preset-react": "^7.17.12",
"@babel/preset-typescript": "^7.17.12",
"@edge-runtime/jest-environment": "1.1.0-beta.35",
"@next-auth/tsconfig": "workspace:*",
"@swc/core": "^1.2.198",
"@swc/jest": "^0.2.21",
"@testing-library/dom": "^8.13.0",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/react-hooks": "^8.0.0",
"@testing-library/user-event": "^14.2.0",
"@types/jest": "^28.1.3",
"@types/node": "^17.0.42",
"@types/nodemailer": "^6.4.4",
"@types/oauth": "^0.9.1",
"@types/react": "18.0.37",
"@types/react-dom": "^18.0.6",
"autoprefixer": "^10.4.7",
"babel-plugin-jsx-pragmatic": "^1.0.2",
"babel-preset-preact": "^2.0.0",
"concurrently": "^7",
"cssnano": "^5.1.11",
"jest": "^28.1.1",
"jest-environment-jsdom": "^28.1.1",
"jest-watch-typeahead": "^1.1.0",
"msw": "^0.42.3",
"next": "13.3.0",
"postcss": "^8.4.14",
"postcss-cli": "^9.1.0",
"postcss-nested": "^5.0.6",
"react": "^18",
"react-dom": "^18",
"whatwg-fetch": "^3.6.2"
}
}

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="4 32 376.4 449.4" width="32" height="32">
<title>Apple icon</title>
<path fill="#fff" d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 588 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="4 32 376.4 449.4" width="32" height="32">
<title>Apple icon</title>
<path d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 576 B

View File

@@ -0,0 +1,8 @@
<svg viewBox="0.29136862699701993 -41.138268758326056 145.22149045698177 186.73799623391153" xmlns="http://www.w3.org/2000/svg" width="32" height="32">
<linearGradient id="a" gradientTransform="matrix(1 0 0 -1 0 228)" gradientUnits="userSpaceOnUse" x1="62.57" x2="25.03" y1="150.13" y2="85.11">
<stop offset="0" stop-color="#0052cc"/>
<stop offset=".92" stop-color="#2684ff"/>
</linearGradient>
<path d="M43 67a4.14 4.14 0 0 0-5.79-.78A4.29 4.29 0 0 0 36 67.73L.45 138.85a4.25 4.25 0 0 0 1.9 5.7 4.18 4.18 0 0 0 1.9.45h49.53a4.08 4.08 0 0 0 3.8-2.35C68.27 120.57 61.79 87 43 67z" fill="url(#a)"/>
<path d="M69.13 2.28a93.82 93.82 0 0 0-5.48 92.61l23.88 47.76a4.25 4.25 0 0 0 3.8 2.35h49.52a4.24 4.24 0 0 0 4.25-4.25 4.31 4.31 0 0 0-.44-1.9L76.36 2.26a4 4 0 0 0-7.23 0z" fill="#2684ff"/>
</svg>

After

Width:  |  Height:  |  Size: 810 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0.29136862699701993 -41.138268758326056 145.22149045698177 186.73799623391153" xmlns="http://www.w3.org/2000/svg" width="32" height="32">
<path d="M43 67a4.14 4.14 0 0 0-5.79-.78A4.29 4.29 0 0 0 36 67.73L.45 138.85a4.25 4.25 0 0 0 1.9 5.7 4.18 4.18 0 0 0 1.9.45h49.53a4.08 4.08 0 0 0 3.8-2.35C68.27 120.57 61.79 87 43 67z" fill="#fff"/>
<path d="M69.13 2.28a93.82 93.82 0 0 0-5.48 92.61l23.88 47.76a4.25 4.25 0 0 0 3.8 2.35h49.52a4.24 4.24 0 0 0 4.25-4.25 4.31 4.31 0 0 0-.44-1.9L76.36 2.26a4 4 0 0 0-7.23 0z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 549 B

View File

@@ -0,0 +1,12 @@
<svg width="32" height="32" viewBox="0 0 41 45" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M35.3018 0H20.5L25.0737 14.076H39.8755L27.9009 22.4701L32.4746 36.6253C40.1827 31.081 42.7027 22.6883 39.8755 14.076L35.3018 0Z" fill="white"/>
<path d="M1.12504 14.076H15.9268L20.5005 0H5.69875L1.12504 14.076C-1.70213 22.6898 0.8178 31.081 8.52592 36.6253L13.0996 22.4701L1.12504 14.076Z" fill="white"/>
<path d="M8.52539 36.6251L20.5 44.9998L32.4746 36.6251L20.5 28.1084L8.52539 36.6251Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="41" height="45" fill="none"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 677 B

View File

@@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 256 287" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet">
<path d="M203.24 231.531l-28.73-88.434 75.208-54.64h-92.966L128.019.025l-.009-.024h92.98l28.74 88.446.002-.002.024-.013c16.69 51.31-.5 109.67-46.516 143.098zm-150.45 0l-.023.017 75.228 54.655 75.245-54.67-75.221-54.656-75.228 54.654zM6.295 88.434c-17.57 54.088 2.825 111.4 46.481 143.108l.007-.028 28.735-88.429-75.192-54.63h92.944L128.004.024 128.01 0H35.025L6.294 88.434z" fill="#EB5424"/>
</svg>

After

Width:  |  Height:  |  Size: 523 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 59.242 47.271" width="32" height="32" xmlns="http://www.w3.org/2000/svg">
<path d="m32.368 0-17.468 15.145-14.9 26.75h13.437zm2.323 3.543-7.454 21.008 14.291 17.956-27.728 4.764h45.442z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 228 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 59.242 47.271" width="32" height="32" xmlns="http://www.w3.org/2000/svg">
<path d="m32.368 0-17.468 15.145-14.9 26.75h13.437zm2.323 3.543-7.454 21.008 14.291 17.956-27.728 4.764h45.442z" fill="#0072c6"/>
</svg>

After

Width:  |  Height:  |  Size: 231 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 580.44" width="32" height="32">
<path d="M473.49,197.63c-75.94-35.11-185.08-57.42-287.78-49.11,5.15-34,17.85-57.69,38.7-62.68,28.69-6.88,60,12,89.84,46.35,19.55,2.53,42.73,7,58.86,10.71C318.7,40.56,245.72-16.8,190.21,4.36,148,20.47,126.39,78.59,129,156.69c-55,11.7-97.87,32.49-125.31,62.39-1.39,1.61-4.53,5.67-3.41,7.61.85,1.47,3.65-.18,4.85-1,31.83-22.26,72.58-34.31,125.66-41.89,7.56,83.32,42.81,189,101.36,273.78-32,12.56-58.89,13.39-73.64-2.17-20.29-21.41-19.61-57.95-4.77-101-7.58-18.2-15.31-40.51-20.15-56.34C72.12,396.41,58.93,488.29,105,525.78c35.07,28.52,96.18,18.15,162.54-23.12,37.64,41.79,77.07,68.51,116.69,77.33,2.09.39,7.17,1.09,8.29-.85.85-1.47-2-3.08-3.28-3.71-35.19-16.44-66-45.71-99.1-87.88C358.53,439.34,432.42,356,476.57,262.88c26.9,21.47,41,44.3,34.94,64.85-8.4,28.29-40.38,46-85.06,54.63C414.48,398,399,415.86,387.74,428c115.84,4,202-30.47,211.43-89.12,7.17-44.64-32.37-92.38-101.3-129.21,17.38-53.49,20.8-101,8.63-139.72-.71-2-2.64-6.76-4.88-6.76-1.7,0-1.68,3.26-1.58,4.7C503.41,106.55,493.47,147.88,473.49,197.63ZM260.21,444.33c-49-78.61-77.24-171.21-77.06-264.84h0C275.71,176.39,370,198.2,451,245.17h0c-43.59,81.71-109.64,152.49-190.82,199.15Z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 580.44" width="32" height="32">
<path d="M473.49,197.63c-75.94-35.11-185.08-57.42-287.78-49.11,5.15-34,17.85-57.69,38.7-62.68,28.69-6.88,60,12,89.84,46.35,19.55,2.53,42.73,7,58.86,10.71C318.7,40.56,245.72-16.8,190.21,4.36,148,20.47,126.39,78.59,129,156.69c-55,11.7-97.87,32.49-125.31,62.39-1.39,1.61-4.53,5.67-3.41,7.61.85,1.47,3.65-.18,4.85-1,31.83-22.26,72.58-34.31,125.66-41.89,7.56,83.32,42.81,189,101.36,273.78-32,12.56-58.89,13.39-73.64-2.17-20.29-21.41-19.61-57.95-4.77-101-7.58-18.2-15.31-40.51-20.15-56.34C72.12,396.41,58.93,488.29,105,525.78c35.07,28.52,96.18,18.15,162.54-23.12,37.64,41.79,77.07,68.51,116.69,77.33,2.09.39,7.17,1.09,8.29-.85.85-1.47-2-3.08-3.28-3.71-35.19-16.44-66-45.71-99.1-87.88C358.53,439.34,432.42,356,476.57,262.88c26.9,21.47,41,44.3,34.94,64.85-8.4,28.29-40.38,46-85.06,54.63C414.48,398,399,415.86,387.74,428c115.84,4,202-30.47,211.43-89.12,7.17-44.64-32.37-92.38-101.3-129.21,17.38-53.49,20.8-101,8.63-139.72-.71-2-2.64-6.76-4.88-6.76-1.7,0-1.68,3.26-1.58,4.7C503.41,106.55,493.47,147.88,473.49,197.63ZM260.21,444.33c-49-78.61-77.24-171.21-77.06-264.84h0C275.71,176.39,370,198.2,451,245.17h0c-43.59,81.71-109.64,152.49-190.82,199.15Z" fill="#148eff"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 444.893 245.414">
<g fill="#fff">
<path d="M239.038 72.43c-33.081 0-61.806 18.6-76.322 45.904-14.516-27.305-43.24-45.902-76.32-45.902-19.443 0-37.385 6.424-51.821 17.266V16.925h-.008C34.365 7.547 26.713 0 17.286 0 7.858 0 .208 7.547.008 16.925H0v143.333h.036c.768 47.051 39.125 84.967 86.359 84.967 33.08 0 61.805-18.603 76.32-45.908 14.517 27.307 43.241 45.906 76.321 45.906 47.715 0 86.396-38.684 86.396-86.396.001-47.718-38.682-86.397-86.394-86.397zM86.395 210.648c-28.621 0-51.821-23.201-51.821-51.82 0-28.623 23.201-51.823 51.821-51.823 28.621 0 51.822 23.2 51.822 51.823 0 28.619-23.201 51.82-51.822 51.82zm152.643 0c-28.622 0-51.821-23.201-51.821-51.822 0-28.623 23.2-51.821 51.821-51.821 28.619 0 51.822 23.198 51.822 51.821-.001 28.621-23.203 51.822-51.822 51.822z"/>
<path d="M441.651 218.033l-44.246-59.143 44.246-59.144-.008-.007c5.473-7.62 3.887-18.249-3.652-23.913-7.537-5.658-18.187-4.221-23.98 3.157l-.004-.002-38.188 51.047-38.188-51.047-.006.009c-5.793-7.385-16.441-8.822-23.981-3.16-7.539 5.664-9.125 16.293-3.649 23.911l-.008.005 44.245 59.144-44.245 59.143.008.005c-5.477 7.62-3.89 18.247 3.649 23.909 7.54 5.664 18.188 4.225 23.981-3.155l.006.007 38.188-51.049 38.188 51.049.004-.002c5.794 7.377 16.443 8.814 23.98 3.154 7.539-5.662 9.125-16.291 3.652-23.91l.008-.008z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 444.893 245.414">
<g fill="#0075C9">
<path d="M239.038 72.43c-33.081 0-61.806 18.6-76.322 45.904-14.516-27.305-43.24-45.902-76.32-45.902-19.443 0-37.385 6.424-51.821 17.266V16.925h-.008C34.365 7.547 26.713 0 17.286 0 7.858 0 .208 7.547.008 16.925H0v143.333h.036c.768 47.051 39.125 84.967 86.359 84.967 33.08 0 61.805-18.603 76.32-45.908 14.517 27.307 43.241 45.906 76.321 45.906 47.715 0 86.396-38.684 86.396-86.396.001-47.718-38.682-86.397-86.394-86.397zM86.395 210.648c-28.621 0-51.821-23.201-51.821-51.82 0-28.623 23.201-51.823 51.821-51.823 28.621 0 51.822 23.2 51.822 51.823 0 28.619-23.201 51.82-51.822 51.82zm152.643 0c-28.622 0-51.821-23.201-51.821-51.822 0-28.623 23.2-51.821 51.821-51.821 28.619 0 51.822 23.198 51.822 51.821-.001 28.621-23.203 51.822-51.822 51.822z"/>
<path d="M441.651 218.033l-44.246-59.143 44.246-59.144-.008-.007c5.473-7.62 3.887-18.249-3.652-23.913-7.537-5.658-18.187-4.221-23.98 3.157l-.004-.002-38.188 51.047-38.188-51.047-.006.009c-5.793-7.385-16.441-8.822-23.981-3.16-7.539 5.664-9.125 16.293-3.649 23.911l-.008.005 44.245 59.144-44.245 59.143.008.005c-5.477 7.62-3.89 18.247 3.649 23.909 7.54 5.664 18.188 4.225 23.981-3.155l.006.007 38.188-51.049 38.188 51.049.004-.002c5.794 7.377 16.443 8.814 23.98 3.154 7.539-5.662 9.125-16.291 3.652-23.91l.008-.008z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,10 @@
<svg width="32" height="32" viewBox="0 0 256 299" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid">
<path d="M208.752 58.061l25.771-6.636.192.283.651 155.607-.843.846-5.31.227-20.159-3.138-.302-.794V58.061M59.705 218.971l.095.007 68.027 19.767.173.133.296.236-.096 59.232-.2.252-68.295-33.178v-46.449" fill="#7A3E65"/>
<path d="M208.752 204.456l-80.64 19.312-40.488-9.773-27.919 4.976L128 238.878l105.405-28.537 1.118-2.18-25.771-3.705" fill="#CFB2C1"/>
<path d="M196.295 79.626l-.657-.749-66.904-19.44-.734.283-.672-.343L22.052 89.734l-.575.703.845.463 24.075 3.53.851-.289 80.64-19.311 40.488 9.773 27.919-4.977" fill="#512843"/>
<path d="M47.248 240.537l-25.771 6.221-.045-.149-1.015-155.026 1.06-1.146 25.771 3.704v146.396" fill="#C17B9E"/>
<path d="M82.04 180.403l45.96 5.391.345-.515.187-71.887-.532-.589-45.96 5.392v62.208" fill="#7A3E65"/>
<path d="M173.96 180.403L128 185.794v-72.991l45.96 5.392v62.208M196.295 79.626L128 59.72V0l68.295 33.177v46.449" fill="#C17B9E"/>
<path d="M128 0L0 61.793v175.011l21.477 9.954V90.437L128 59.72V0" fill="#7A3E65"/>
<path d="M234.523 51.425v156.736L128 238.878v59.72l128-61.794V61.793l-21.477-10.368" fill="#C17B9E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 256 293" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid">
<path d="M226.011 0H29.99C13.459 0 0 13.458 0 30.135v197.778c0 16.677 13.458 30.135 29.989 30.135h165.888l-7.754-27.063 18.725 17.408 17.7 16.384L256 292.571V30.135C256 13.458 242.542 0 226.011 0zm-56.466 191.05s-5.266-6.291-9.655-11.85c19.164-5.413 26.478-17.408 26.478-17.408-5.998 3.95-11.703 6.73-16.823 8.63-7.314 3.073-14.336 5.12-21.211 6.291-14.044 2.633-26.917 1.902-37.888-.146-8.339-1.61-15.507-3.95-21.504-6.29-3.365-1.317-7.022-2.926-10.68-4.974-.438-.293-.877-.439-1.316-.732-.292-.146-.439-.292-.585-.438-2.633-1.463-4.096-2.487-4.096-2.487s7.022 11.703 25.6 17.261c-4.388 5.56-9.801 12.142-9.801 12.142-32.33-1.024-44.617-22.235-44.617-22.235 0-47.104 21.065-85.285 21.065-85.285 21.065-15.799 41.106-15.36 41.106-15.36l1.463 1.756C80.75 77.53 68.608 89.088 68.608 89.088s3.218-1.755 8.63-4.242c15.653-6.876 28.088-8.777 33.208-9.216.877-.147 1.609-.293 2.487-.293a123.776 123.776 0 0 1 29.55-.292c13.896 1.609 28.818 5.705 44.031 14.043 0 0-11.556-10.971-36.425-18.578l2.048-2.34s20.041-.44 41.106 15.36c0 0 21.066 38.18 21.066 85.284 0 0-12.435 21.211-44.764 22.235zm-68.023-68.316c-8.338 0-14.92 7.314-14.92 16.237 0 8.924 6.728 16.238 14.92 16.238 8.339 0 14.921-7.314 14.921-16.238.147-8.923-6.582-16.237-14.92-16.237m53.394 0c-8.339 0-14.922 7.314-14.922 16.237 0 8.924 6.73 16.238 14.922 16.238 8.338 0 14.92-7.314 14.92-16.238 0-8.923-6.582-16.237-14.92-16.237" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 256 293" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid">
<path d="M226.011 0H29.99C13.459 0 0 13.458 0 30.135v197.778c0 16.677 13.458 30.135 29.989 30.135h165.888l-7.754-27.063 18.725 17.408 17.7 16.384L256 292.571V30.135C256 13.458 242.542 0 226.011 0zm-56.466 191.05s-5.266-6.291-9.655-11.85c19.164-5.413 26.478-17.408 26.478-17.408-5.998 3.95-11.703 6.73-16.823 8.63-7.314 3.073-14.336 5.12-21.211 6.291-14.044 2.633-26.917 1.902-37.888-.146-8.339-1.61-15.507-3.95-21.504-6.29-3.365-1.317-7.022-2.926-10.68-4.974-.438-.293-.877-.439-1.316-.732-.292-.146-.439-.292-.585-.438-2.633-1.463-4.096-2.487-4.096-2.487s7.022 11.703 25.6 17.261c-4.388 5.56-9.801 12.142-9.801 12.142-32.33-1.024-44.617-22.235-44.617-22.235 0-47.104 21.065-85.285 21.065-85.285 21.065-15.799 41.106-15.36 41.106-15.36l1.463 1.756C80.75 77.53 68.608 89.088 68.608 89.088s3.218-1.755 8.63-4.242c15.653-6.876 28.088-8.777 33.208-9.216.877-.147 1.609-.293 2.487-.293a123.776 123.776 0 0 1 29.55-.292c13.896 1.609 28.818 5.705 44.031 14.043 0 0-11.556-10.971-36.425-18.578l2.048-2.34s20.041-.44 41.106 15.36c0 0 21.066 38.18 21.066 85.284 0 0-12.435 21.211-44.764 22.235zm-68.023-68.316c-8.338 0-14.92 7.314-14.92 16.237 0 8.924 6.728 16.238 14.92 16.238 8.339 0 14.921-7.314 14.921-16.238.147-8.923-6.582-16.237-14.92-16.237m53.394 0c-8.339 0-14.922 7.314-14.922 16.237 0 8.924 6.73 16.238 14.922 16.238 8.338 0 14.92-7.314 14.92-16.238 0-8.923-6.582-16.237-14.92-16.237" fill="#7289DA"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,4 @@
<svg clip-rule="evenodd" fill-rule="evenodd" width="32" height="32" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" viewBox="6702.77 18309.17 6561.66 6561.660000000007" xmlns="http://www.w3.org/2000/svg">
<path d="M9983.6 18309.17c1811.95 0 3280.83 1468.88 3280.83 3280.83s-1468.88 3280.83-3280.83 3280.83S6702.77 23401.95 6702.77 21590s1468.88-3280.83 3280.83-3280.83z" fill="#fff"/>
<path d="M10409.89 24843.29v-2534.17h714.43l94.7-891.91h-809.13l1.2-446.44c0-232.63 22.1-357.22 356.24-357.22h446.68v-892.06h-714.59c-858.35 0-1160.42 432.65-1160.42 1160.34v535.45h-535.07v891.99H9339v2498.09c208.45 41.53 423.95 63.47 644.6 63.47a3310.9 3310.9 0 0 0 426.29-27.54z" fill="#006aff" fill-rule="nonzero"/>
</svg>

After

Width:  |  Height:  |  Size: 774 B

View File

@@ -0,0 +1,8 @@
<svg clip-rule="evenodd" fill-rule="evenodd" width="32" height="32" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" viewBox="6702.77 18309.17 6561.66 6561.660000000007" xmlns="http://www.w3.org/2000/svg">
<linearGradient id="a" gradientUnits="userSpaceOnUse" x1="9983.6" x2="9983.6" y1="18249.39" y2="25150.62">
<stop offset="0" stop-color="#00b2ff"/>
<stop offset="1" stop-color="#006aff"/>
</linearGradient>
<path d="M9983.6 18309.17c1811.95 0 3280.83 1468.88 3280.83 3280.83s-1468.88 3280.83-3280.83 3280.83S6702.77 23401.95 6702.77 21590s1468.88-3280.83 3280.83-3280.83z" fill="url(#a)"/>
<path d="M10409.89 24843.29v-2534.17h714.43l94.7-891.91h-809.13l1.2-446.44c0-232.63 22.1-357.22 356.24-357.22h446.68v-892.06h-714.59c-858.35 0-1160.42 432.65-1160.42 1160.34v535.45h-535.07v891.99H9339v2498.09c208.45 41.53 423.95 63.47 644.6 63.47a3310.9 3310.9 0 0 0 426.29-27.54z" fill="#fff" fill-rule="nonzero"/>
</svg>

After

Width:  |  Height:  |  Size: 991 B

View File

@@ -0,0 +1,18 @@
<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 275.9 275.9" style="enable-background:new 0 0 275.9 275.9;">
<style type="text/css">
.st0{enable-background:new ;}
.st1{fill:#FFFFFF;}
</style>
<rect x="0.1" y="0.3" width="275.4" height="275.6"/>
<g class="st0">
<path class="st1" d="M69.2,50.1H121v8.5H77.8v23.8h38.4v8.5H77.8v34.4h-8.6C69.2,125.2,69.2,50.1,69.2,50.1z"/>
<path class="st1" d="M67.7,198.8l8.4-1.9c1.5,10.4,8.7,16.6,20.4,16.6c10.5,0,18.8-4.9,18.8-12.9c0-5.7-4.2-10.5-20.6-15.4
c-18.6-5.3-24.8-12.1-24.8-22.1c0-12.9,10.4-19.8,25.5-19.8c16.9,0,24.4,8.6,27,20.4l-8.4,1.9c-2.1-9.7-8.7-13.8-18.9-13.8
c-9.6,0-16.4,3.6-16.4,10.5c0,5.6,4.4,9.9,19.6,14.7c18.1,5.6,25.9,11.7,25.9,23.3c0,14.4-12.1,21.9-27.5,21.9
C80.6,222.1,69.6,213.8,67.7,198.8z"/>
<path class="st1" d="M134.5,182.9c0-22.3,14.6-39.7,37-39.7c22.3,0,36.7,17.5,36.7,39.7c0,11.1-3.7,20.7-9.9,27.7
c3,3,5.9,6,8.8,9.2l-6.2,6c-3-3.2-6.1-6.4-9.2-9.5c-5.7,3.7-12.5,5.8-20.2,5.8C149.9,222.1,134.5,205.6,134.5,182.9z M185.2,209.9
c-2.9-2.8-5.8-5.5-8.8-8.1l6-6.1c3.2,2.8,6.4,5.7,9.4,8.6c4.5-5.4,7.2-12.8,7.2-21.5c0-17.6-10.7-31-27.5-31s-27.6,13.4-27.6,31
c0,18.1,11.7,30.8,27.6,30.8C176.6,213.6,181.2,212.4,185.2,209.9z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,18 @@
<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 275.9 275.9" style="enable-background:new 0 0 275.9 275.9;">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{enable-background:new ;}
</style>
<rect x="0.1" y="0.3" class="st0" width="275.4" height="275.6"/>
<g class="st1">
<path d="M69.2,50.1H121v8.5H77.8v23.8h38.4v8.5H77.8v34.4h-8.6C69.2,125.2,69.2,50.1,69.2,50.1z"/>
<path d="M67.7,198.8l8.4-1.9c1.5,10.4,8.7,16.6,20.4,16.6c10.5,0,18.8-4.9,18.8-12.9c0-5.7-4.2-10.5-20.6-15.4
c-18.6-5.3-24.8-12.1-24.8-22.1c0-12.9,10.4-19.8,25.5-19.8c16.9,0,24.4,8.6,27,20.4l-8.4,1.9c-2.1-9.7-8.7-13.8-18.9-13.8
c-9.6,0-16.4,3.6-16.4,10.5c0,5.6,4.4,9.9,19.6,14.7c18.1,5.6,25.9,11.7,25.9,23.3c0,14.4-12.1,21.9-27.5,21.9
C80.6,222.1,69.6,213.8,67.7,198.8z"/>
<path d="M134.5,182.9c0-22.3,14.6-39.7,37-39.7c22.3,0,36.7,17.5,36.7,39.7c0,11.1-3.7,20.7-9.9,27.7c3,3,5.9,6,8.8,9.2l-6.2,6
c-3-3.2-6.1-6.4-9.2-9.5c-5.7,3.7-12.5,5.8-20.2,5.8C149.9,222.1,134.5,205.6,134.5,182.9z M185.2,209.9c-2.9-2.8-5.8-5.5-8.8-8.1
l6-6.1c3.2,2.8,6.4,5.7,9.4,8.6c4.5-5.4,7.2-12.8,7.2-21.5c0-17.6-10.7-31-27.5-31s-27.6,13.4-27.6,31c0,18.1,11.7,30.8,27.6,30.8
C176.6,213.6,181.2,212.4,185.2,209.9z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox=".99522558 .9999996 253.69877442 253.6940004" xmlns="http://www.w3.org/2000/svg">
<path d="m107.948 1a106.948 106.948 0 0 0 -106.948 106.966v146.728h146.727c59.067 0 106.955-47.88 106.967-106.948v-146.746zm86.724 43.635a34.6 34.6 0 0 1 -10.164 24.51 34.768 34.768 0 0 1 -24.562 10.152h-37.242v29.73h50.314v34.796h-50.065v71.663h-41.233v-179.891h40.983v32.625c1.072-18.32 16.275-32.625 34.668-32.625h37.358z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 467 B

View File

@@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox=".99522558 .9999996 253.69877442 253.6940004" xmlns="http://www.w3.org/2000/svg">
<path d="m107.948 1a106.948 106.948 0 0 0 -106.948 106.966v146.728h146.727c59.067 0 106.955-47.88 106.967-106.948v-146.746zm86.724 43.635a34.6 34.6 0 0 1 -10.164 24.51 34.768 34.768 0 0 1 -24.562 10.152h-37.242v29.73h50.314v34.796h-50.065v71.663h-41.233v-179.891h40.983v32.625c1.072-18.32 16.275-32.625 34.668-32.625h37.358z" fill="#0075dd"/>
</svg>

After

Width:  |  Height:  |  Size: 470 B

View File

@@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>GitHub icon</title>
<path fill="#fff" d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
</svg>

After

Width:  |  Height:  |  Size: 859 B

View File

@@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>GitHub dark icon</title>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
</svg>

After

Width:  |  Height:  |  Size: 852 B

View File

@@ -0,0 +1,8 @@
<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg" viewBox="93.97 97.52 192.05 184.95">
<defs>
<style>.cls-1{fill:#fff;}</style>
</defs>
<g id="LOGO">
<path class="cls-1" d="M282.83,170.73l-.27-.69-26.14-68.22a6.81,6.81,0,0,0-2.69-3.24,7,7,0,0,0-8,.43,7,7,0,0,0-2.32,3.52l-17.65,54H154.29l-17.65-54A6.86,6.86,0,0,0,134.32,99a7,7,0,0,0-8-.43,6.87,6.87,0,0,0-2.69,3.24L97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82,19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91,40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 591 B

View File

@@ -0,0 +1,11 @@
<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg" viewBox="93.97 97.52 192.05 184.99">
<defs>
<style>.cls-1{fill:#e24329;}.cls-2{fill:#fc6d26;}.cls-3{fill:#fca326;}</style>
</defs>
<g>
<path class="cls-1" d="M282.83,170.73l-.27-.69-26.14-68.22a6.81,6.81,0,0,0-2.69-3.24,7,7,0,0,0-8,.43,7,7,0,0,0-2.32,3.52l-17.65,54H154.29l-17.65-54A6.86,6.86,0,0,0,134.32,99a7,7,0,0,0-8-.43,6.87,6.87,0,0,0-2.69,3.24L97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82,19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91,40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/>
<path class="cls-2" d="M282.83,170.73l-.27-.69a88.3,88.3,0,0,0-35.15,15.8L190,229.25c19.55,14.79,36.57,27.64,36.57,27.64l40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/>
<path class="cls-3" d="M153.43,256.89l19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91S209.55,244,190,229.25C170.45,244,153.43,256.89,153.43,256.89Z"/>
<path class="cls-2" d="M132.58,185.84A88.19,88.19,0,0,0,97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82s17-12.85,36.57-27.64Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="32" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Google icon</title>
<path fill="#EA4335 " d="M5.26620003,9.76452941 C6.19878754,6.93863203 8.85444915,4.90909091 12,4.90909091 C13.6909091,4.90909091 15.2181818,5.50909091 16.4181818,6.49090909 L19.9090909,3 C17.7818182,1.14545455 15.0545455,0 12,0 C7.27006974,0 3.1977497,2.69829785 1.23999023,6.65002441 L5.26620003,9.76452941 Z"/>
<path fill="#34A853" d="M16.0407269,18.0125889 C14.9509167,18.7163016 13.5660892,19.0909091 12,19.0909091 C8.86648613,19.0909091 6.21911939,17.076871 5.27698177,14.2678769 L1.23746264,17.3349879 C3.19279051,21.2936293 7.26500293,24 12,24 C14.9328362,24 17.7353462,22.9573905 19.834192,20.9995801 L16.0407269,18.0125889 Z"/>
<path fill="#4A90E2" d="M19.834192,20.9995801 C22.0291676,18.9520994 23.4545455,15.903663 23.4545455,12 C23.4545455,11.2909091 23.3454545,10.5272727 23.1818182,9.81818182 L12,9.81818182 L12,14.4545455 L18.4363636,14.4545455 C18.1187732,16.013626 17.2662994,17.2212117 16.0407269,18.0125889 L19.834192,20.9995801 Z"/>
<path fill="#FBBC05" d="M5.27698177,14.2678769 C5.03832634,13.556323 4.90909091,12.7937589 4.90909091,12 C4.90909091,11.2182781 5.03443647,10.4668121 5.26620003,9.76452941 L1.23999023,6.65002441 C0.43658717,8.26043162 0,10.0753848 0,12 C0,13.9195484 0.444780743,15.7301709 1.23746264,17.3349879 L5.27698177,14.2678769 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="6.20856283 .64498824 244.26943717 251.24701176" xmlns="http://www.w3.org/2000/svg">
<path d="m191.385 85.694v-29.506a22.722 22.722 0 0 0 13.101-20.48v-.677c0-12.549-10.173-22.722-22.721-22.722h-.678c-12.549 0-22.722 10.173-22.722 22.722v.677a22.722 22.722 0 0 0 13.101 20.48v29.506a64.342 64.342 0 0 0 -30.594 13.47l-80.922-63.03c.577-2.083.878-4.225.912-6.375a25.6 25.6 0 1 0 -25.633 25.55 25.323 25.323 0 0 0 12.607-3.43l79.685 62.007c-14.65 22.131-14.258 50.974.987 72.7l-24.236 24.243c-1.96-.626-4-.959-6.057-.987-11.607.01-21.01 9.423-21.007 21.03.003 11.606 9.412 21.014 21.018 21.017 11.607.003 21.02-9.4 21.03-21.007a20.747 20.747 0 0 0 -.988-6.056l23.976-23.985c21.423 16.492 50.846 17.913 73.759 3.562 22.912-14.352 34.475-41.446 28.985-67.918-5.49-26.473-26.873-46.734-53.603-50.792m-9.938 97.044a33.17 33.17 0 1 1 0-66.316c17.85.625 32 15.272 32.01 33.134.008 17.86-14.127 32.522-31.977 33.165" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 967 B

View File

@@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="6.20856283 .64498824 244.26943717 251.24701176" xmlns="http://www.w3.org/2000/svg">
<path d="m191.385 85.694v-29.506a22.722 22.722 0 0 0 13.101-20.48v-.677c0-12.549-10.173-22.722-22.721-22.722h-.678c-12.549 0-22.722 10.173-22.722 22.722v.677a22.722 22.722 0 0 0 13.101 20.48v29.506a64.342 64.342 0 0 0 -30.594 13.47l-80.922-63.03c.577-2.083.878-4.225.912-6.375a25.6 25.6 0 1 0 -25.633 25.55 25.323 25.323 0 0 0 12.607-3.43l79.685 62.007c-14.65 22.131-14.258 50.974.987 72.7l-24.236 24.243c-1.96-.626-4-.959-6.057-.987-11.607.01-21.01 9.423-21.007 21.03.003 11.606 9.412 21.014 21.018 21.017 11.607.003 21.02-9.4 21.03-21.007a20.747 20.747 0 0 0 -.988-6.056l23.976-23.985c21.423 16.492 50.846 17.913 73.759 3.562 22.912-14.352 34.475-41.446 28.985-67.918-5.49-26.473-26.873-46.734-53.603-50.792m-9.938 97.044a33.17 33.17 0 1 1 0-66.316c17.85.625 32 15.272 32.01 33.134.008 17.86-14.127 32.522-31.977 33.165" fill="#ff7a59"/>
</svg>

After

Width:  |  Height:  |  Size: 970 B

View File

@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="2.842170943040401e-14 0 3364.7 3364.7" width="32" height="32">
<defs>
<radialGradient id="a" cx="217.76" cy="3290.99" r="4271.92" gradientUnits="userSpaceOnUse">
<stop offset=".09" stop-color="#fa8f21"/>
<stop offset=".78" stop-color="#d82d7e"/>
</radialGradient>
<radialGradient id="b" cx="2330.61" cy="3182.95" r="3759.33" gradientUnits="userSpaceOnUse">
<stop offset=".64" stop-color="#8c3aaa" stop-opacity="0"/>
<stop offset="1" stop-color="#8c3aaa"/>
</radialGradient>
</defs>
<path d="M853.2 3352.8c-200.1-9.1-308.8-42.4-381.1-70.6-95.8-37.3-164.1-81.7-236-153.5s-116.4-140.1-153.5-235.9c-28.2-72.3-61.5-181-70.6-381.1-10-216.3-12-281.2-12-829.2s2.2-612.8 11.9-829.3C21 653.1 54.5 544.6 82.5 472.1 119.8 376.3 164.3 308 236 236c71.8-71.8 140.1-116.4 236-153.5C544.3 54.3 653 21 853.1 11.9 1069.5 2 1134.5 0 1682.3 0c548 0 612.8 2.2 829.3 11.9 200.1 9.1 308.6 42.6 381.1 70.6 95.8 37.1 164.1 81.7 236 153.5s116.2 140.2 153.5 236c28.2 72.3 61.5 181 70.6 381.1 9.9 216.5 11.9 281.3 11.9 829.3 0 547.8-2 612.8-11.9 829.3-9.1 200.1-42.6 308.8-70.6 381.1-37.3 95.8-81.7 164.1-153.5 235.9s-140.2 116.2-236 153.5c-72.3 28.2-181 61.5-381.1 70.6-216.3 9.9-281.3 11.9-829.3 11.9-547.8 0-612.8-1.9-829.1-11.9" fill="url(#a)"/>
<path d="M853.2 3352.8c-200.1-9.1-308.8-42.4-381.1-70.6-95.8-37.3-164.1-81.7-236-153.5s-116.4-140.1-153.5-235.9c-28.2-72.3-61.5-181-70.6-381.1-10-216.3-12-281.2-12-829.2s2.2-612.8 11.9-829.3C21 653.1 54.5 544.6 82.5 472.1 119.8 376.3 164.3 308 236 236c71.8-71.8 140.1-116.4 236-153.5C544.3 54.3 653 21 853.1 11.9 1069.5 2 1134.5 0 1682.3 0c548 0 612.8 2.2 829.3 11.9 200.1 9.1 308.6 42.6 381.1 70.6 95.8 37.1 164.1 81.7 236 153.5s116.2 140.2 153.5 236c28.2 72.3 61.5 181 70.6 381.1 9.9 216.5 11.9 281.3 11.9 829.3 0 547.8-2 612.8-11.9 829.3-9.1 200.1-42.6 308.8-70.6 381.1-37.3 95.8-81.7 164.1-153.5 235.9s-140.2 116.2-236 153.5c-72.3 28.2-181 61.5-381.1 70.6-216.3 9.9-281.3 11.9-829.3 11.9-547.8 0-612.8-1.9-829.1-11.9" fill="url(#b)"/>
<path d="M1269.25 1689.52c0-230.11 186.49-416.7 416.6-416.7s416.7 186.59 416.7 416.7-186.59 416.7-416.7 416.7-416.6-186.59-416.6-416.7m-225.26 0c0 354.5 287.36 641.86 641.86 641.86s641.86-287.36 641.86-641.86-287.36-641.86-641.86-641.86S1044 1335 1044 1689.52m1159.13-667.31a150 150 0 1 0 150.06-149.94h-.06a150.07 150.07 0 0 0-150 149.94M1180.85 2707c-121.87-5.55-188.11-25.85-232.13-43-58.36-22.72-100-49.78-143.78-93.5s-70.88-85.32-93.5-143.68c-17.16-44-37.46-110.26-43-232.13-6.06-131.76-7.27-171.34-7.27-505.15s1.31-373.28 7.27-505.15c5.55-121.87 26-188 43-232.13 22.72-58.36 49.78-100 93.5-143.78s85.32-70.88 143.78-93.5c44-17.16 110.26-37.46 232.13-43 131.76-6.06 171.34-7.27 505-7.27S2059.13 666 2191 672c121.87 5.55 188 26 232.13 43 58.36 22.62 100 49.78 143.78 93.5s70.78 85.42 93.5 143.78c17.16 44 37.46 110.26 43 232.13 6.06 131.87 7.27 171.34 7.27 505.15s-1.21 373.28-7.27 505.15c-5.55 121.87-25.95 188.11-43 232.13-22.72 58.36-49.78 100-93.5 143.68s-85.42 70.78-143.78 93.5c-44 17.16-110.26 37.46-232.13 43-131.76 6.06-171.34 7.27-505.15 7.27s-373.28-1.21-505-7.27M1170.5 447.09c-133.07 6.06-224 27.16-303.41 58.06-82.19 31.91-151.86 74.72-221.43 144.18S533.39 788.47 501.48 870.76c-30.9 79.46-52 170.34-58.06 303.41-6.16 133.28-7.57 175.89-7.57 515.35s1.41 382.07 7.57 515.35c6.06 133.08 27.16 223.95 58.06 303.41 31.91 82.19 74.62 152 144.18 221.43s139.14 112.18 221.43 144.18c79.56 30.9 170.34 52 303.41 58.06 133.35 6.06 175.89 7.57 515.35 7.57s382.07-1.41 515.35-7.57c133.08-6.06 223.95-27.16 303.41-58.06 82.19-32 151.86-74.72 221.43-144.18s112.18-139.24 144.18-221.43c30.9-79.46 52.1-170.34 58.06-303.41 6.06-133.38 7.47-175.89 7.47-515.35s-1.41-382.07-7.47-515.35c-6.06-133.08-27.16-224-58.06-303.41-32-82.19-74.72-151.86-144.18-221.43s-139.24-112.27-221.33-144.18c-79.56-30.9-170.44-52.1-303.41-58.06-133.3-6.09-175.89-7.57-515.3-7.57s-382.1 1.41-515.45 7.57" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,260 @@
<svg
id="Layer_1"
data-name="Layer 1"
viewBox="0 0 512 512"
version="1.1"
width="32"
height="32"
sodipodi:docname="keycloak_icon_512px.svg"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview113"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="2.1434174"
inkscape:cx="279.92681"
inkscape:cy="239.33742"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="g110" />
<defs
id="defs10">
<style
id="style2">.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}.cls-3{fill:#b17c81;stroke:#b17c81;}.cls-10,.cls-11,.cls-12,.cls-13,.cls-3,.cls-4,.cls-5,.cls-6,.cls-7,.cls-8,.cls-9{stroke-width:1.51px;}.cls-4{fill:#a2747c;stroke:#a2747c;}.cls-5{fill:#996976;stroke:#996976;}.cls-6{fill:#aa787e;stroke:#aa787e;}.cls-7{fill:#b2777e;stroke:#b2777e;}.cls-8{fill:#b27a7f;stroke:#b27a7f;}.cls-9{fill:#c78485;stroke:#c78485;}.cls-10{fill:#c08184;stroke:#c08184;}.cls-11{fill:#c48485;stroke:#c48485;}.cls-12{fill:#d58b88;stroke:#d58b88;}.cls-13{fill:#e09790;stroke:#e09790;}.cls-14{clip-path:url(#clip-path-2);}.cls-15{fill:#4d4d4d;}.cls-16{fill:#e1e1e1;}.cls-17{fill:#c8c8c8;}.cls-18{fill:#c2c2c2;}.cls-19{fill:#c7c7c7;}.cls-20{fill:#cecece;}.cls-21{fill:#d3d3d3;}.cls-22{fill:#c6c6c6;}.cls-23{fill:#d5d5d5;}.cls-24{fill:#d0d0d0;}.cls-25{fill:#bfbfbf;}.cls-26{fill:#d9d9d9;}.cls-27{fill:#d4d4d4;}.cls-28{fill:#d8d8d8;}.cls-29{fill:#e2e2e2;}.cls-30{fill:#e4e4e4;}.cls-31{fill:#dedede;}.cls-32{fill:#c5c5c5;}.cls-33{fill:#d1d1d1;}.cls-34{fill:#ddd;}.cls-35{fill:#e3e3e3;}.cls-36{fill:#00b8e3;}.cls-37{fill:#33c6e9;}.cls-38{fill:#008aaa;}</style>
<clipPath
id="clip-path">
<rect
class="cls-1"
x="-1018.62"
y="565.7"
width="1881.24"
height="1175.78"
id="rect4" />
</clipPath>
<clipPath
id="clip-path-2">
<rect
class="cls-1"
width="512"
height="512"
id="rect7" />
</clipPath>
</defs>
<title
id="title12">keycloak_deliverables</title>
<g
class="cls-2"
clip-path="url(#clip-path)"
id="g36">
<path
class="cls-3"
d="M-42.82,358l245,24.8,199.4,2Z"
id="path14" />
<path
class="cls-4"
d="M-42.82,358l444.44,26.79,227.18-2Z"
id="path16" />
<path
class="cls-5"
d="M401.62,384.74L565.31,523.63,628.8,382.76Z"
id="path18" />
<path
class="cls-6"
d="M202.22,382.76l54.56,14.88,144.84-12.9Z"
id="path20" />
<path
class="cls-7"
d="M401.62,384.74L356,537.52l209.32-13.89Z"
id="path22" />
<path
class="cls-8"
d="M256.78,397.64L356,537.52l45.63-152.78Z"
id="path24" />
<path
class="cls-9"
d="M256.78,397.64L164.52,533.55l191.47,4Z"
id="path26" />
<path
class="cls-10"
d="M202.22,382.76l-37.7,150.79,92.26-135.91Z"
id="path28" />
<path
class="cls-11"
d="M-42.82,358L164.52,533.55l37.7-150.79Z"
id="path30" />
<path
class="cls-12"
d="M-42.82,358l-51.59,137.9,258.93,37.7Z"
id="path32" />
<path
class="cls-13"
d="M-94.41,495.85L-33.89,598l198.41-64.48Z"
id="path34" />
</g>
<g
class="cls-14"
clip-path="url(#clip-path-2)"
id="g110">
<path
class="cls-15"
d="m 438.48,152 c -1.36647,0.0146 -2.635,-0.70753 -3.32,-1.89 L 377.39,49.94 C 376.67452,48.754034 375.38499,48.035096 374,48.05 H 138.33 c -1.37003,-0.01824 -2.64324,0.704388 -3.33,1.89 L 75,153.89 19.17,254.09 c -0.670238,1.18514 -0.670238,2.63486 0,3.82 L 75,358 135,462 c 0.685,1.18247 1.95353,1.90461 3.32,1.89 H 374 c 1.37433,0.004 2.64999,-0.71328 3.36,-1.89 L 435.2,361.9 c 0.685,-1.18247 1.95353,-1.90461 3.32,-1.89 h 71.93 c 2.38587,0 4.32,-1.93413 4.32,-4.32 V 156.32 c 0,-2.38587 -1.93413,-4.32 -4.32,-4.32 h -72 z"
id="path38"
sodipodi:nodetypes="ccccccccccccccccsssscc" />
<path
style="fill:#e2e2e2;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
d="M 72.85,157.64 17.658505,256.00902 23.53,268.94 78.375,358.53222 114.19,360 401.46,359.98 H 461 L 499.26418,350.30026 514.76,307.49 514.77,258.13 V 212.41 L 510.46,152 h -71.98 l -22.11,0.01 H 367.16 147.94 l -75.09,5.63"
id="path27674" />
<path
class="cls-1"
d="M510.46,152H78.34A3.91,3.91,0,0,0,75,153.89s0,0.07,0,.07l-2.14,3.69L46.41,203.48,17.18,254.11a3.8,3.8,0,0,0,0,3.83l6.35,11L75,358.06A3.84,3.84,0,0,0,78.34,360H510.52a4.27,4.27,0,0,0,4.24-4.28V156.34A4.32,4.32,0,0,0,510.46,152Z"
id="path40" />
<path
class="cls-16"
d="M88.1,245.5L23.53,268.94l-6.35-11a3.8,3.8,0,0,1,0-3.83l29.23-50.63Z"
id="path42" />
<polygon
class="cls-17"
points="472.21 264.21 514.77 258.13 514.77 307.49 472.21 264.21"
id="polygon44" />
<path
class="cls-18"
d="M472.21,264.21l42.55,43.28V355.7a4.27,4.27,0,0,1-4.24,4.28H461Z"
id="path46" />
<polygon
class="cls-19"
points="472.21 264.21 461 359.98 401.46 359.98 383.42 316.53 472.21 264.21"
id="polygon48" />
<polygon
class="cls-20"
points="472.21 264.21 514.77 212.41 514.77 258.13 472.21 264.21"
id="polygon50" />
<path
class="cls-21"
d="M514.77,156.33v56.08l-42.55,51.8L440.12,152h70.33A4.32,4.32,0,0,1,514.77,156.33Z"
id="path52" />
<polygon
class="cls-22"
points="401.46 359.98 370.06 359.98 361.92 348.31 383.42 316.53 401.46 359.98"
id="polygon54" />
<polygon
class="cls-23"
points="472.21 264.21 354.42 214.42 416.37 152.01 440.12 152.01 472.21 264.21"
id="polygon56" />
<path
class="cls-24"
d="M354.42,214.42l29,102.11,88.8-52.32Z"
id="path58" />
<polygon
class="cls-25"
points="370.06 359.98 361.54 359.98 361.92 348.31 370.06 359.98"
id="polygon60" />
<polygon
class="cls-26"
points="416.37 152.01 354.42 214.42 343.24 158.6 367.16 152.01 416.37 152.01"
id="polygon62" />
<path
class="cls-27"
d="M354.42,214.42l-143,33L361.92,348.31Z"
id="path64" />
<path
class="cls-24"
d="M354.42,214.42l7.49,133.9,21.5-31.78Z"
id="path66" />
<path
class="cls-26"
d="M343.24,158.6L211.47,247.39l143-33Z"
id="path68" />
<polygon
class="cls-28"
points="211.47 247.39 149.5 359.98 114.19 359.98 88.1 245.5 211.47 247.39"
id="polygon70" />
<path
class="cls-29"
d="M147.94,152L88.1,245.5,72.85,157.64,75,153.94s0-.07,0-0.07A3.91,3.91,0,0,1,78.33,152h69.61Z"
id="path72" />
<path
class="cls-28"
d="M114.19,360H78.33A3.84,3.84,0,0,1,75,358L23.53,268.94,88.1,245.5Z"
id="path74" />
<polygon
class="cls-30"
points="46.41,203.47 72.85,157.64 88.1,245.5 "
id="polygon76" />
<polygon
class="cls-31"
points="276.77 152.01 235.53 152.01 172.39 152.01 211.47 247.39 343.24 158.6 303.52 152.01 276.77 152.01"
id="polygon78" />
<polygon
class="cls-31"
points="156.09 152.01 147.94 152.01 88.1 245.5 211.47 247.39 172.39 152.01 156.09 152.01"
id="polygon80" />
<polygon
class="cls-32"
points="333.23 359.98 356.22 359.98 361.54 359.98 361.92 348.31 333.23 359.98"
id="polygon82" />
<polygon
class="cls-24"
points="361.92 348.31 211.47 247.39 238.57 359.98 276.77 359.98 333.23 359.98 361.92 348.31"
id="polygon84" />
<polygon
class="cls-33"
points="149.5 359.98 156.09 359.98 235.53 359.98 238.57 359.98 211.47 247.39 149.5 359.98"
id="polygon86" />
<polygon
class="cls-34"
points="343.65 152.01 343.24 158.6 367.16 152.01 356.22 152.01 343.65 152.01"
id="polygon88" />
<polygon
class="cls-35"
points="303.52 152.01 343.24 158.6 339.58 152.01 303.52 152.01"
id="polygon90" />
<polygon
class="cls-29"
points="339.58 152.01 343.24 158.6 343.65 152.01 339.58 152.01"
id="polygon92" />
<path
class="cls-36"
d="M235.15,153.81L177,254.46a3.38,3.38,0,0,0-.42,1.64H136.07l79.74-138.18a3.14,3.14,0,0,1,1.19,1.15l0.11,0.11,18.08,31.41A3.49,3.49,0,0,1,235.15,153.81Z"
id="path94" />
<path
class="cls-37"
d="M235.08,361.89l-18,31.27a3.51,3.51,0,0,1-1.22,1.15L136,256.14v0H176.6a3.09,3.09,0,0,0,.38,1.57,0.37,0.37,0,0,0,.07.17l58,100.58A3.41,3.41,0,0,1,235.08,361.89Z"
id="path96" />
<path
class="cls-38"
d="M215.81,117.92L136.07,256.1h0v0l-20,34.66-19.1-33.12a3.09,3.09,0,0,1-.38-1.57,3.38,3.38,0,0,1,.42-1.64L116.31,221l58.75-101.74a3.4,3.4,0,0,1,3-1.75H214.1A3.58,3.58,0,0,1,215.81,117.92Z"
id="path98" />
<path
class="cls-36"
d="M215.81,394.31a3.58,3.58,0,0,1-1.71.45H178a3.4,3.4,0,0,1-3-1.75l-53.72-93L116,290.79l20-34.66Z"
id="path100" />
<path
class="cls-38"
d="M376.19,256.1L296.39,394.31a3.73,3.73,0,0,1-1.19-1.15l-0.07-.1L277,361.72a3.49,3.49,0,0,1,0-3.22l58.06-100.65a3.38,3.38,0,0,0,.49-1.75h40.57Z"
id="path102" />
<path
class="cls-36"
d="M415.68,256.1a3.38,3.38,0,0,1-.49,1.75L337.06,393.16a3.42,3.42,0,0,1-2.9,1.61h-36a3.72,3.72,0,0,1-1.75-.45L376.19,256.1l20-34.62,19,32.91A3.35,3.35,0,0,1,415.68,256.1Z"
id="path104" />
<path
class="cls-36"
d="M376.19,256.1H335.63a3.35,3.35,0,0,0-.49-1.71l-58-100.55a3.41,3.41,0,0,1,0-3.46l18.08-31.3a3.73,3.73,0,0,1,1.19-1.15Z"
id="path106" />
<path
class="cls-37"
d="M396.2,221.44v0l-20,34.62L296.39,117.92a3.72,3.72,0,0,1,1.75-.45h36a3.42,3.42,0,0,1,2.9,1.61Z"
id="path108" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,6 @@
<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 377.764 377.764">
<title>Line icon</title>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#00C300" d="M77.315 0h223.133c42.523 0 77.315 34.792 77.315 77.315v223.133c0 42.523-34.792 77.315-77.315 77.315H77.315C34.792 377.764 0 342.972 0 300.448V77.315C0 34.792 34.792 0 77.315 0z"/>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFF" d="M188.515 62.576c76.543 0 138.593 49.687 138.593 110.979 0 21.409-7.576 41.398-20.691 58.351-.649.965-1.497 2.031-2.566 3.209l-.081.088c-4.48 5.36-9.525 10.392-15.072 15.037-38.326 35.425-101.41 77.601-109.736 71.094-7.238-5.656 11.921-33.321-10.183-37.925-1.542-.177-3.08-.367-4.605-.583l-.029-.002v-.002c-64.921-9.223-114.222-54.634-114.222-109.267-.002-61.292 62.049-110.979 138.592-110.979z"/>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#00C300" d="M108.103 208.954h27.952c3.976 0 7.228-3.253 7.228-7.229v-.603c0-3.976-3.252-7.228-7.228-7.228h-20.121v-45.779c0-3.976-3.252-7.228-7.228-7.228h-.603c-3.976 0-7.228 3.252-7.228 7.228v53.609c0 3.977 3.252 7.23 7.228 7.23zm173.205-33.603v-.603c0-3.976-3.253-7.228-7.229-7.228h-20.12v-11.445h20.12c3.976 0 7.229-3.252 7.229-7.228v-.603c0-3.976-3.253-7.228-7.229-7.228h-27.952c-3.976 0-7.228 3.252-7.228 7.228v53.609c0 3.976 3.252 7.229 7.228 7.229h27.952c3.976 0 7.229-3.253 7.229-7.229v-.603c0-3.976-3.253-7.228-7.229-7.228h-20.12v-11.445h20.12c3.976.002 7.229-3.251 7.229-7.226zm-53.755 31.448l.002-.003a7.207 7.207 0 0 0 2.09-5.07v-53.609c0-3.976-3.252-7.228-7.229-7.228h-.603c-3.976 0-7.228 3.252-7.228 7.228v31.469l-26.126-35.042c-1.248-2.179-3.598-3.655-6.276-3.655h-.603c-3.976 0-7.229 3.252-7.229 7.228v53.609c0 3.976 3.252 7.229 7.229 7.229h.603c3.976 0 7.228-3.253 7.228-7.229v-32.058l26.314 35.941c.162.252.339.494.53.724l.001.002c.723.986 1.712 1.662 2.814 2.075.847.35 1.773.544 2.742.544h.603a7.162 7.162 0 0 0 3.377-.844c.723-.344 1.332-.788 1.761-1.311zm-71.208 2.155h.603c3.976 0 7.228-3.253 7.228-7.229v-53.609c0-3.976-3.252-7.228-7.228-7.228h-.603c-3.976 0-7.229 3.252-7.229 7.228v53.609c0 3.976 3.253 7.229 7.229 7.229z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,6 @@
<svg height="32" width="32" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<g fill="none">
<path d="M0 18.338C0 8.216 8.474 0 18.92 0h218.16C247.53 0 256 8.216 256 18.338v219.327C256 247.79 247.53 256 237.08 256H18.92C8.475 256 0 247.791 0 237.668V18.335z" fill="#fff"/>
<path d="M77.796 214.238V98.986H39.488v115.252H77.8zM58.65 83.253c13.356 0 21.671-8.85 21.671-19.91-.25-11.312-8.315-19.915-21.417-19.915-13.111 0-21.674 8.603-21.674 19.914 0 11.06 8.312 19.91 21.169 19.91h.248zM99 214.238h38.305v-64.355c0-3.44.25-6.889 1.262-9.346 2.768-6.885 9.071-14.012 19.656-14.012 13.858 0 19.405 10.568 19.405 26.063v61.65h38.304v-66.082c0-35.399-18.896-51.872-44.099-51.872-20.663 0-29.738 11.549-34.78 19.415h.255V98.99H99.002c.5 10.812-.003 115.252-.003 115.252z" fill="#069"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 813 B

View File

@@ -0,0 +1,6 @@
<svg height="32" width="32" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<g fill="none">
<path d="M0 18.338C0 8.216 8.474 0 18.92 0h218.16C247.53 0 256 8.216 256 18.338v219.327C256 247.79 247.53 256 237.08 256H18.92C8.475 256 0 247.791 0 237.668V18.335z" fill="#069"/>
<path d="M77.796 214.238V98.986H39.488v115.252H77.8zM58.65 83.253c13.356 0 21.671-8.85 21.671-19.91-.25-11.312-8.315-19.915-21.417-19.915-13.111 0-21.674 8.603-21.674 19.914 0 11.06 8.312 19.91 21.169 19.91h.248zM99 214.238h38.305v-64.355c0-3.44.25-6.889 1.262-9.346 2.768-6.885 9.071-14.012 19.656-14.012 13.858 0 19.405 10.568 19.405 26.063v61.65h38.304v-66.082c0-35.399-18.896-51.872-44.099-51.872-20.663 0-29.738 11.549-34.78 19.415h.255V98.99H99.002c.5 10.812-.003 115.252-.003 115.252z" fill="#fff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 813 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width="32" height="32">
<path fill="#ffffff" fill-rule="evenodd" d="M37.03 25.32c.57 0 1.47.66 1.47 2.26 0 1.58-.65 3.38-.8 3.77C35.32 37.03 29.7 40.2 23 40c-6.24-.19-11.57-3.5-13.9-8.9a6.13 6.13 0 0 1-3.97-1.6 5.9 5.9 0 0 1-2.02-3.78 6.5 6.5 0 0 1 .37-2.99l-1.31-1.11C-3.83 16.52 14.92-4.42 20.9.84l2.04 2.01 1.12-.47c5.26-2.2 9.52-1.14 9.53 2.36 0 1.81-1.15 3.93-3 5.85.67.62 1.2 1.6 1.52 2.7.25.83.3 1.67.32 2.2l.07 2.5.74.2c1.42.4 2.42.93 2.91 1.45.5.52.73 1.02.82 1.6a3.1 3.1 0 0 1-.55 2.26s.16.35.32.85l.28.97zm-14.56 2.63zm14.63.16c.15-.95-.06-1.31-.35-1.49-.3-.19-.66-.12-.66-.12s-.17-1.13-.63-2.16a13.83 13.83 0 0 1-4.53 2.26c-1.56.45-3.68.8-6.04.65-1.31-.1-2.18-.49-2.5.58 2.99 1.1 6.16.63 6.16.63.06 0 .11.04.12.1 0 .05-.02.1-.07.12 0 0-2.43 1.13-6.3-.07.1.91.99 1.32 1.41 1.49.53.2 1.12.3 1.12.3 4.79.83 9.27-1.92 10.28-2.62.07-.05.12 0 .06.1l-.1.13c-1.23 1.6-4.55 3.46-8.87 3.46-1.88 0-3.76-.67-4.45-1.7-1.08-1.58-.06-3.9 1.73-3.66l.78.09a16.3 16.3 0 0 0 8.13-1.31c2.44-1.14 3.36-2.4 3.22-3.4a1.46 1.46 0 0 0-.42-.83 5.25 5.25 0 0 0-2.3-1.1c-.39-.1-.65-.18-.93-.27-.5-.17-.76-.3-.81-1.25l-.13-2.47c-.04-1.05-.17-2.48-1.05-3.07a1.48 1.48 0 0 0-.76-.25c-.26 0-.4.04-.45.05-.5.08-.8.35-1.18.67A4.04 4.04 0 0 1 24.51 14c-.62-.03-1.28-.13-2.03-.17l-.44-.03c-1.73-.08-3.6 1.42-3.9 3.56-.44 2.98 1.7 4.52 2.33 5.43.08.1.17.26.17.4 0 .17-.11.31-.22.43a7.66 7.66 0 0 0-1.36 8.03c1.57 3.68 6.43 5.4 11.18 3.84.63-.21 1.23-.47 1.8-.77 1.07-.52 2-1.24 2.76-2.07a8.27 8.27 0 0 0 2.3-4.54zm-7.9-9.2a3.23 3.23 0 0 1-.52-1.28c-.2-.96-.18-1.65.37-1.74.56-.09.82.49 1.02 1.44.14.64.11 1.23-.04 1.57a3.2 3.2 0 0 0-.82 0zm-4.74.75c-.4-.18-.91-.37-1.53-.34-.88.06-1.65.45-1.87.42-.09-.01-.13-.05-.14-.1-.04-.17.22-.44.48-.63.8-.58 1.84-.71 2.72-.33.42.18.82.5 1.02.83.1.15.11.27.05.33-.1.1-.34-.01-.73-.18zm-.8.45c.71-.08 1.23.25 1.35.45.05.08.03.14.02.16-.06.1-.18.08-.44.05a3.27 3.27 0 0 0-1.66.17s-.26.1-.38.1a.12.12 0 0 1-.12-.12c0-.1.1-.26.25-.4.18-.16.46-.33.98-.4zm3.94 1.68c-.35-.17-.53-.52-.4-.78.12-.26.5-.32.86-.15.35.17.53.52.4.78-.12.25-.5.32-.86.15zm2.25-1.98c.29 0 .51.33.5.72 0 .4-.23.7-.52.7-.28 0-.5-.32-.5-.72 0-.39.24-.7.52-.7zm-14.77-8.58c-.06.06.02.15.09.1A15.1 15.1 0 0 1 27.1 8.94c.07.02.12-.11.05-.15-1-.57-2.54-.95-3.63-.96-.06 0-.09-.06-.05-.1.19-.26.44-.51.68-.7.05-.04.02-.12-.05-.12-1.55.1-3.32.84-4.34 1.54-.05.04-.12 0-.1-.07.07-.38.32-.89.45-1.13.04-.05-.03-.11-.08-.08a17.67 17.67 0 0 0-4.95 4.06zm-7.72 8.2c1.71-4.61 4.57-8.87 8.35-11.8 2.81-2.35 5.84-4.04 5.84-4.04s-1.63-1.9-2.12-2.04C16.4.73 9.85 5.26 5.67 11.25c-1.69 2.43-4.1 6.73-2.95 8.94.14.27.95.97 1.38 1.34a5.15 5.15 0 0 1 3.26-2.1zm2.26 10.14c2.19-.37 2.76-2.76 2.4-5.1-.4-2.66-2.2-3.6-3.4-3.66-.34-.02-.65.01-.9.06-2.17.44-3.39 2.29-3.15 4.7.22 2.16 2.4 4 4.43 4.05.2 0 .41-.01.62-.05zm.83-2.72c.1-.03.22-.06.3.03.02.03.06.1.01.21-.08.19-.4.44-.85.43-.47-.04-1-.38-1.06-1.23-.04-.42.12-.94.22-1.2a1.12 1.12 0 0 0-1.3-1.52c-.3.07-.54.24-.7.5a2.64 2.64 0 0 0-.3.7c-.1.27-.25.35-.36.33-.05 0-.12-.04-.17-.16-.12-.34-.02-1.29.61-1.98a1.9 1.9 0 0 1 1.63-.6c.63.09 1.16.47 1.48 1.09.43.82.05 1.68-.18 2.2l-.06.15c-.15.34-.16.64-.03.84.1.15.28.24.49.24.1 0 .19-.02.27-.03z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width="32" height="32">
<path fill="#000000" fill-rule="evenodd" d="M37.03 25.32c.57 0 1.47.66 1.47 2.26 0 1.58-.65 3.38-.8 3.77C35.32 37.03 29.7 40.2 23 40c-6.24-.19-11.57-3.5-13.9-8.9a6.13 6.13 0 0 1-3.97-1.6 5.9 5.9 0 0 1-2.02-3.78 6.5 6.5 0 0 1 .37-2.99l-1.31-1.11C-3.83 16.52 14.92-4.42 20.9.84l2.04 2.01 1.12-.47c5.26-2.2 9.52-1.14 9.53 2.36 0 1.81-1.15 3.93-3 5.85.67.62 1.2 1.6 1.52 2.7.25.83.3 1.67.32 2.2l.07 2.5.74.2c1.42.4 2.42.93 2.91 1.45.5.52.73 1.02.82 1.6a3.1 3.1 0 0 1-.55 2.26s.16.35.32.85l.28.97zm-14.56 2.63zm14.63.16c.15-.95-.06-1.31-.35-1.49-.3-.19-.66-.12-.66-.12s-.17-1.13-.63-2.16a13.83 13.83 0 0 1-4.53 2.26c-1.56.45-3.68.8-6.04.65-1.31-.1-2.18-.49-2.5.58 2.99 1.1 6.16.63 6.16.63.06 0 .11.04.12.1 0 .05-.02.1-.07.12 0 0-2.43 1.13-6.3-.07.1.91.99 1.32 1.41 1.49.53.2 1.12.3 1.12.3 4.79.83 9.27-1.92 10.28-2.62.07-.05.12 0 .06.1l-.1.13c-1.23 1.6-4.55 3.46-8.87 3.46-1.88 0-3.76-.67-4.45-1.7-1.08-1.58-.06-3.9 1.73-3.66l.78.09a16.3 16.3 0 0 0 8.13-1.31c2.44-1.14 3.36-2.4 3.22-3.4a1.46 1.46 0 0 0-.42-.83 5.25 5.25 0 0 0-2.3-1.1c-.39-.1-.65-.18-.93-.27-.5-.17-.76-.3-.81-1.25l-.13-2.47c-.04-1.05-.17-2.48-1.05-3.07a1.48 1.48 0 0 0-.76-.25c-.26 0-.4.04-.45.05-.5.08-.8.35-1.18.67A4.04 4.04 0 0 1 24.51 14c-.62-.03-1.28-.13-2.03-.17l-.44-.03c-1.73-.08-3.6 1.42-3.9 3.56-.44 2.98 1.7 4.52 2.33 5.43.08.1.17.26.17.4 0 .17-.11.31-.22.43a7.66 7.66 0 0 0-1.36 8.03c1.57 3.68 6.43 5.4 11.18 3.84.63-.21 1.23-.47 1.8-.77 1.07-.52 2-1.24 2.76-2.07a8.27 8.27 0 0 0 2.3-4.54zm-7.9-9.2a3.23 3.23 0 0 1-.52-1.28c-.2-.96-.18-1.65.37-1.74.56-.09.82.49 1.02 1.44.14.64.11 1.23-.04 1.57a3.2 3.2 0 0 0-.82 0zm-4.74.75c-.4-.18-.91-.37-1.53-.34-.88.06-1.65.45-1.87.42-.09-.01-.13-.05-.14-.1-.04-.17.22-.44.48-.63.8-.58 1.84-.71 2.72-.33.42.18.82.5 1.02.83.1.15.11.27.05.33-.1.1-.34-.01-.73-.18zm-.8.45c.71-.08 1.23.25 1.35.45.05.08.03.14.02.16-.06.1-.18.08-.44.05a3.27 3.27 0 0 0-1.66.17s-.26.1-.38.1a.12.12 0 0 1-.12-.12c0-.1.1-.26.25-.4.18-.16.46-.33.98-.4zm3.94 1.68c-.35-.17-.53-.52-.4-.78.12-.26.5-.32.86-.15.35.17.53.52.4.78-.12.25-.5.32-.86.15zm2.25-1.98c.29 0 .51.33.5.72 0 .4-.23.7-.52.7-.28 0-.5-.32-.5-.72 0-.39.24-.7.52-.7zm-14.77-8.58c-.06.06.02.15.09.1A15.1 15.1 0 0 1 27.1 8.94c.07.02.12-.11.05-.15-1-.57-2.54-.95-3.63-.96-.06 0-.09-.06-.05-.1.19-.26.44-.51.68-.7.05-.04.02-.12-.05-.12-1.55.1-3.32.84-4.34 1.54-.05.04-.12 0-.1-.07.07-.38.32-.89.45-1.13.04-.05-.03-.11-.08-.08a17.67 17.67 0 0 0-4.95 4.06zm-7.72 8.2c1.71-4.61 4.57-8.87 8.35-11.8 2.81-2.35 5.84-4.04 5.84-4.04s-1.63-1.9-2.12-2.04C16.4.73 9.85 5.26 5.67 11.25c-1.69 2.43-4.1 6.73-2.95 8.94.14.27.95.97 1.38 1.34a5.15 5.15 0 0 1 3.26-2.1zm2.26 10.14c2.19-.37 2.76-2.76 2.4-5.1-.4-2.66-2.2-3.6-3.4-3.66-.34-.02-.65.01-.9.06-2.17.44-3.39 2.29-3.15 4.7.22 2.16 2.4 4 4.43 4.05.2 0 .41-.01.62-.05zm.83-2.72c.1-.03.22-.06.3.03.02.03.06.1.01.21-.08.19-.4.44-.85.43-.47-.04-1-.38-1.06-1.23-.04-.42.12-.94.22-1.2a1.12 1.12 0 0 0-1.3-1.52c-.3.07-.54.24-.7.5a2.64 2.64 0 0 0-.3.7c-.1.27-.25.35-.36.33-.05 0-.12-.04-.17-.16-.12-.34-.02-1.29.61-1.98a1.9 1.9 0 0 1 1.63-.6c.63.09 1.16.47 1.48 1.09.43.82.05 1.68-.18 2.2l-.06.15c-.15.34-.16.64-.03.84.1.15.28.24.49.24.1 0 .19-.02.27-.03z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,3 @@
<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<path d="M32 0C14.37 0 0 14.267 0 32s14.268 32 32 32 32-14.268 32-32S49.63 0 32 0zm0 48c-8.866 0-16-7.134-16-16s7.134-16 16-16 16 7.134 16 16-7.134 16-16 16z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 266 B

View File

@@ -0,0 +1,3 @@
<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<path d="M32 0C14.37 0 0 14.267 0 32s14.268 32 32 32 32-14.268 32-32S49.63 0 32 0zm0 48c-8.866 0-16-7.134-16-16s7.134-16 16-16 16 7.134 16 16-7.134 16-16 16z" fill="#007dc1"/>
</svg>

After

Width:  |  Height:  |  Size: 269 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 567.18 545.8" xmlns="http://www.w3.org/2000/svg" width="32" height="32">
<circle cx="362.59" cy="204.59" fill="#e85b46" r="204.59"/>
<path d="M0 0h100v545.8H0z" fill="#241e12"/>
</svg>

After

Width:  |  Height:  |  Size: 207 B

View File

@@ -0,0 +1,8 @@
<svg enable-background="new 0 0 2447.6 2452.5" viewBox="0 0 2447.6 2452.5" xmlns="http://www.w3.org/2000/svg" width="32" height="32">
<g clip-rule="evenodd" fill-rule="evenodd">
<path d="m897.4 0c-135.3.1-244.8 109.9-244.7 245.2-.1 135.3 109.5 245.1 244.8 245.2h244.8v-245.1c.1-135.3-109.5-245.1-244.9-245.3.1 0 .1 0 0 0m0 654h-652.6c-135.3.1-244.9 109.9-244.8 245.2-.2 135.3 109.4 245.1 244.7 245.3h652.7c135.3-.1 244.9-109.9 244.8-245.2.1-135.4-109.5-245.2-244.8-245.3z" fill="#36c5f0"/>
<path d="m2447.6 899.2c.1-135.3-109.5-245.1-244.8-245.2-135.3.1-244.9 109.9-244.8 245.2v245.3h244.8c135.3-.1 244.9-109.9 244.8-245.3zm-652.7 0v-654c.1-135.2-109.4-245-244.7-245.2-135.3.1-244.9 109.9-244.8 245.2v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.3z" fill="#2eb67d"/>
<path d="m1550.1 2452.5c135.3-.1 244.9-109.9 244.8-245.2.1-135.3-109.5-245.1-244.8-245.2h-244.8v245.2c-.1 135.2 109.5 245 244.8 245.2zm0-654.1h652.7c135.3-.1 244.9-109.9 244.8-245.2.2-135.3-109.4-245.1-244.7-245.3h-652.7c-135.3.1-244.9 109.9-244.8 245.2-.1 135.4 109.4 245.2 244.7 245.3z" fill="#ecb22e"/>
<path d="m0 1553.2c-.1 135.3 109.5 245.1 244.8 245.2 135.3-.1 244.9-109.9 244.8-245.2v-245.2h-244.8c-135.3.1-244.9 109.9-244.8 245.2zm652.7 0v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.2v-653.9c.2-135.3-109.4-245.1-244.7-245.3-135.4 0-244.9 109.8-244.8 245.1 0 0 0 .1 0 0" fill="#e01e5a"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2931 2931" width="32" height="32">
<style>.st0{fill:#2ebd59} </style>
<path class="st0" d="M1465.5 0C656.1 0 0 656.1 0 1465.5S656.1 2931 1465.5 2931 2931 2274.9 2931 1465.5C2931 656.2 2274.9.1 1465.5 0zm672.1 2113.6c-26.3 43.2-82.6 56.7-125.6 30.4-344.1-210.3-777.3-257.8-1287.4-141.3-49.2 11.3-98.2-19.5-109.4-68.7-11.3-49.2 19.4-98.2 68.7-109.4C1242.1 1697.1 1721 1752 2107.3 1988c43 26.5 56.7 82.6 30.3 125.6zm179.3-398.9c-33.1 53.8-103.5 70.6-157.2 37.6-393.8-242.1-994.4-312.2-1460.3-170.8-60.4 18.3-124.2-15.8-142.6-76.1-18.2-60.4 15.9-124.1 76.2-142.5 532.2-161.5 1193.9-83.3 1646.2 194.7 53.8 33.1 70.8 103.4 37.7 157.1zm15.4-415.6c-472.4-280.5-1251.6-306.3-1702.6-169.5-72.4 22-149-18.9-170.9-91.3-21.9-72.4 18.9-149 91.4-171 517.7-157.1 1378.2-126.8 1922 196 65.1 38.7 86.5 122.8 47.9 187.8-38.5 65.2-122.8 86.7-187.8 48z"/>
</svg>

After

Width:  |  Height:  |  Size: 899 B

View File

@@ -0,0 +1,8 @@
<svg width="32" height="32" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid">
<g>
<path d="M224.001997,0 L31.9980026,0 C14.3579381,0.0394964443 0.0614809418,14.336846 0,32 L0,224 C0,241.6 14.3971038,256 31.9980026,256 L224.001997,256 C241.602896,256 256,241.6 256,224 L256,32 C256,14.4 241.602896,0 224.001997,0" fill="#E44332"></path>
<path d="M54.132778,120.802491 C58.5960224,118.196275 154.476075,62.477451 156.667847,61.1862981 C158.859619,59.9110855 158.97917,55.9898065 156.508446,54.5711324 C154.053661,53.1604284 149.391165,50.4824817 147.661658,49.4543415 C145.192242,48.0957707 142.191169,48.132074 139.755339,49.5499825 C138.527947,50.2672896 56.6035026,97.8486625 53.8697654,99.4107981 C50.5781227,101.291737 46.5372925,101.323617 43.2695601,99.4107981 L0,74.0181257 L0,95.6011002 C10.5205046,101.801822 36.7181549,117.200015 43.062338,120.826401 C46.8481256,122.978322 50.4745117,122.930502 54.1407481,120.802491" fill="#FFFFFF"></path>
<path d="M54.132778,161.609296 C58.5960224,159.00308 154.476075,103.284257 156.667847,101.993104 C158.859619,100.717891 158.97917,96.7966121 156.508446,95.377938 C154.053661,93.9672339 149.391165,91.2892873 147.661658,90.2611471 C145.192242,88.9025763 142.191169,88.9388796 139.755339,90.3567881 C138.527947,91.0740952 56.6035026,138.655468 53.8697654,140.217604 C50.5781227,142.098542 46.5372925,142.130423 43.2695601,140.217604 L0,114.824931 L0,136.407906 C10.5205046,142.608627 36.7181549,158.00682 43.062338,161.633206 C46.8481256,163.785128 50.4745117,163.737307 54.1407481,161.609296" fill="#FFFFFF"></path>
<path d="M54.132778,204.966527 C58.5960224,202.360311 154.476075,146.641487 156.667847,145.350335 C158.859619,144.075122 158.97917,140.153843 156.508446,138.735169 C154.053661,137.324465 149.391165,134.646518 147.661658,133.618378 C145.192242,132.259807 142.191169,132.29611 139.755339,133.714019 C138.527947,134.431326 56.6035026,182.012699 53.8697654,183.574835 C50.5781227,185.455773 46.5372925,185.487654 43.2695601,183.574835 L0,158.182162 L0,179.765137 C10.5205046,185.965858 36.7181549,201.364051 43.062338,204.990437 C46.8481256,207.142359 50.4745117,207.094538 54.1407481,204.966527" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 144.8 144.8" enable-background="new 0 0 144.8 144.8" width="32" height="32">
<g>
<path fill="#FFFFFF" d="M29.5,111.8c10.6,11.6,25.9,18.8,42.9,18.8c8.7,0,16.9-1.9,24.3-5.3L56.3,85L29.5,111.8z"/>
<path fill="#FFFFFF" d="M56.1,60.6L25.5,91.1L21.4,87l32.2-32.2h0l37.6-37.6c-5.9-2-12.2-3.1-18.8-3.1c-32.2,0-58.3,26.1-58.3,58.3
c0,13.1,4.3,25.2,11.7,35l30.5-30.5l2.1,2l43.7,43.7c0.9-0.5,1.7-1,2.5-1.6L56.3,72.7L27,102l-4.1-4.1l33.4-33.4l2.1,2l51,50.9
c0.8-0.6,1.5-1.3,2.2-1.9l-55-55L56.1,60.6z"/>
<path fill="#FFFFFF" d="M115.7,111.4c9.3-10.3,15-24,15-39c0-23.4-13.8-43.5-33.6-52.8L60.4,56.2L115.7,111.4z M74.5,66.8l-4.1-4.1
l28.9-28.9l4.1,4.1L74.5,66.8z M101.9,27.1L68.6,60.4l-4.1-4.1L97.8,23L101.9,27.1z"/>
<g>
<g>
<path fill="#FFFFFF" d="M72.4,144.8C32.5,144.8,0,112.3,0,72.4C0,32.5,32.5,0,72.4,0s72.4,32.5,72.4,72.4
C144.8,112.3,112.3,144.8,72.4,144.8z M72.4,7.3C36.5,7.3,7.3,36.5,7.3,72.4s29.2,65.1,65.1,65.1s65.1-29.2,65.1-65.1
S108.3,7.3,72.4,7.3z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 144.8 144.8" enable-background="new 0 0 144.8 144.8" width="32" height="32">
<g>
<circle fill="#FFFFFF" cx="72.4" cy="72.4" r="72.4"/>
<path fill="#ED2224" d="M29.5,111.8c10.6,11.6,25.9,18.8,42.9,18.8c8.7,0,16.9-1.9,24.3-5.3L56.3,85L29.5,111.8z"/>
<path fill="#ED2224" d="M56.1,60.6L25.5,91.1L21.4,87l32.2-32.2h0l37.6-37.6c-5.9-2-12.2-3.1-18.8-3.1c-32.2,0-58.3,26.1-58.3,58.3
c0,13.1,4.3,25.2,11.7,35l30.5-30.5l2.1,2l43.7,43.7c0.9-0.5,1.7-1,2.5-1.6L56.3,72.7L27,102l-4.1-4.1l33.4-33.4l2.1,2l51,50.9
c0.8-0.6,1.5-1.3,2.2-1.9l-55-55L56.1,60.6z"/>
<path fill="#ED1C24" d="M115.7,111.4c9.3-10.3,15-24,15-39c0-23.4-13.8-43.5-33.6-52.8L60.4,56.2L115.7,111.4z M74.5,66.8l-4.1-4.1
l28.9-28.9l4.1,4.1L74.5,66.8z M101.9,27.1L68.6,60.4l-4.1-4.1L97.8,23L101.9,27.1z"/>
<g>
<g>
<path fill="#ED2224" d="M72.4,144.8C32.5,144.8,0,112.3,0,72.4C0,32.5,32.5,0,72.4,0s72.4,32.5,72.4,72.4
C144.8,112.3,112.3,144.8,72.4,144.8z M72.4,7.3C36.5,7.3,7.3,36.5,7.3,72.4s29.2,65.1,65.1,65.1s65.1-29.2,65.1-65.1
S108.3,7.3,72.4,7.3z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 300 300">
<path fill-rule="evenodd" clip-rule="evenodd" fill="#fff" d="M215.2 260.8h-58.7L117.4 300H78.3v-39.2H6.6V52.2L26.1 0h267.3v182.6l-78.2 78.2zm52.2-91.2V26.1H52.2v189.1h58.7v39.1l39.1-39.1h71.7l45.7-45.6z"/>
<path fill="#fff" d="M195.6 78.3v78.3h26.1V78.3h-26.1zm-71.7 78.2H150V78.3h-26.1v78.2z"/>
</svg>

After

Width:  |  Height:  |  Size: 393 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 300 300">
<path fill-rule="evenodd" clip-rule="evenodd" fill="#65459B" d="M215.2 260.8h-58.7L117.4 300H78.3v-39.2H6.6V52.2L26.1 0h267.3v182.6l-78.2 78.2zm52.2-91.2V26.1H52.2v189.1h58.7v39.1l39.1-39.1h71.7l45.7-45.6z"/>
<path fill="#65459B" d="M195.6 78.3v78.3h26.1V78.3h-26.1zm-71.7 78.2H150V78.3h-26.1v78.2z"/>
</svg>

After

Width:  |  Height:  |  Size: 399 B

View File

@@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="117.806 161.288 464.388 377.424" xmlns="http://www.w3.org/2000/svg">
<path d="m582.194 205.976c-17.078 7.567-35.424 12.68-54.71 14.991 19.675-11.78 34.769-30.474 41.886-52.726-18.407 10.922-38.798 18.857-60.497 23.111-17.385-18.488-42.132-30.064-69.538-30.064-52.603 0-95.266 42.663-95.266 95.307a97.3 97.3 0 0 0 2.454 21.68c-79.211-3.989-149.383-41.928-196.382-99.562-8.18 14.112-12.885 30.474-12.885 47.899 0 33.05 16.833 62.236 42.377 79.314a95.051 95.051 0 0 1 -43.154-11.924v1.227c0 46.16 32.826 84.672 76.43 93.426a95.97 95.97 0 0 1 -25.095 3.313 95.929 95.929 0 0 1 -17.936-1.677c12.128 37.836 47.306 65.406 89.008 66.142-32.622 25.565-73.71 40.802-118.337 40.802-7.69 0-15.278-.45-22.743-1.33 42.173 27.06 92.24 42.807 146.029 42.807 175.275 0 271.094-145.17 271.094-271.073 0-4.09-.103-8.222-.287-12.312 18.612-13.458 34.769-30.208 47.51-49.29z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 915 B

View File

@@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="117.806 161.288 464.388 377.424" xmlns="http://www.w3.org/2000/svg">
<path d="m582.194 205.976c-17.078 7.567-35.424 12.68-54.71 14.991 19.675-11.78 34.769-30.474 41.886-52.726-18.407 10.922-38.798 18.857-60.497 23.111-17.385-18.488-42.132-30.064-69.538-30.064-52.603 0-95.266 42.663-95.266 95.307a97.3 97.3 0 0 0 2.454 21.68c-79.211-3.989-149.383-41.928-196.382-99.562-8.18 14.112-12.885 30.474-12.885 47.899 0 33.05 16.833 62.236 42.377 79.314a95.051 95.051 0 0 1 -43.154-11.924v1.227c0 46.16 32.826 84.672 76.43 93.426a95.97 95.97 0 0 1 -25.095 3.313 95.929 95.929 0 0 1 -17.936-1.677c12.128 37.836 47.306 65.406 89.008 66.142-32.622 25.565-73.71 40.802-118.337 40.802-7.69 0-15.278-.45-22.743-1.33 42.173 27.06 92.24 42.807 146.029 42.807 175.275 0 271.094-145.17 271.094-271.073 0-4.09-.103-8.222-.287-12.312 18.612-13.458 34.769-30.208 47.51-49.29z" fill="#1da1f2"/>
</svg>

After

Width:  |  Height:  |  Size: 918 B

View File

@@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 14.4C0 7.61 0 4.22 2.1 2.1 4.23 0 7.62 0 14.4 0h1.2c6.79 0 10.18 0 12.3 2.1C30 4.23 30 7.62 30 14.4v1.2c0 6.79 0 10.18-2.1 12.3C25.77 30 22.38 30 15.6 30h-1.2c-6.79 0-10.18 0-12.3-2.1C0 25.77 0 22.38 0 15.6v-1.2Z" fill="#fff"> </path>
<path d="M15.96 21.61c-6.84 0-10.74-4.68-10.9-12.48H8.5c.11 5.72 2.63 8.14 4.63 8.64V9.13h3.23v4.93c1.97-.21 4.05-2.46 4.75-4.94h3.22a9.53 9.53 0 0 1-4.38 6.23 9.87 9.87 0 0 1 5.13 6.26h-3.55c-.76-2.37-2.66-4.21-5.17-4.46v4.46h-.39Z" fill="#07F"> </path>
</svg>

After

Width:  |  Height:  |  Size: 609 B

View File

@@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 14.4C0 7.61 0 4.22 2.1 2.1 4.23 0 7.62 0 14.4 0h1.2c6.79 0 10.18 0 12.3 2.1C30 4.23 30 7.62 30 14.4v1.2c0 6.79 0 10.18-2.1 12.3C25.77 30 22.38 30 15.6 30h-1.2c-6.79 0-10.18 0-12.3-2.1C0 25.77 0 22.38 0 15.6v-1.2Z" fill="#07F"> </path>
<path d="M15.96 21.61c-6.84 0-10.74-4.68-10.9-12.48H8.5c.11 5.72 2.63 8.14 4.63 8.64V9.13h3.23v4.93c1.97-.21 4.05-2.46 4.75-4.94h3.22a9.53 9.53 0 0 1-4.38 6.23 9.87 9.87 0 0 1 5.13 6.26h-3.55c-.76-2.37-2.66-4.21-5.17-4.46v4.46h-.39Z" fill="#fff"> </path>
</svg>

After

Width:  |  Height:  |  Size: 609 B

View File

@@ -0,0 +1,10 @@
<svg viewBox="-16 -16 32 32" xmlns="http://www.w3.org/2000/svg" width="32" height="32">
<clipPath id="m">
<path d="m1-2v12h-2v-12l-15-15v33h32v-33z" />
</clipPath>
<g clip-path="url(#m)">
<circle r="9" fill="#fff"/>
<circle fill="none" r="13" stroke="#fff" stroke-width="4"/>
</g>
<circle cy="-10" r="5" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 347 B

View File

@@ -0,0 +1,10 @@
<svg viewBox="-16 -16 32 32" xmlns="http://www.w3.org/2000/svg" width="32" height="32">
<clipPath id="m">
<path d="m1-2v12h-2v-12l-15-15v33h32v-33z"/>
</clipPath>
<g clip-path="url(#m)">
<circle r="9"/>
<circle fill="none" r="13" stroke="#000" stroke-width="4"/>
</g>
<circle cy="-10" r="5"/>
</svg>

After

Width:  |  Height:  |  Size: 301 B

View File

@@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 59 51" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 25.4002C0 26.5139 0.293079 27.6276 0.859699 28.585L11.137 46.3847C12.1921 48.2018 13.7943 49.6867 15.7872 50.351C19.7145 51.6601 23.7785 49.9798 25.7128 46.6192L28.1942 42.3207L18.4054 25.4002L28.7413 7.48329L31.2227 3.1848C31.9652 1.89525 32.9617 0.840161 34.134 0H33.0594H18.1905C15.3964 0 12.8173 1.48494 11.4301 3.90772L0.859699 22.2154C0.293079 23.1728 0 24.2865 0 25.4002Z" fill="#fff"></path>
<path d="M58.6151 25.4001C58.6151 24.2864 58.322 23.1727 57.7554 22.2153L47.3413 4.18118C45.407 0.840078 41.343 -0.840243 37.4157 0.449306C35.4228 1.11362 33.8206 2.59855 32.7655 4.41565L30.4209 8.46014L40.2097 25.4001L29.8738 43.317L27.3924 47.6155C26.6499 48.8856 25.6535 49.9602 24.4811 50.8003H25.5558H40.4247C43.2187 50.8003 45.7978 49.3154 47.185 46.8926L57.7554 28.5849C58.322 27.6275 58.6151 26.5138 58.6151 25.4001Z" fill="#fff"></path>
</svg>

After

Width:  |  Height:  |  Size: 961 B

View File

@@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 59 51" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 25.4002C0 26.5139 0.293079 27.6276 0.859699 28.585L11.137 46.3847C12.1921 48.2018 13.7943 49.6867 15.7872 50.351C19.7145 51.6601 23.7785 49.9798 25.7128 46.6192L28.1942 42.3207L18.4054 25.4002L28.7413 7.48329L31.2227 3.1848C31.9652 1.89525 32.9617 0.840161 34.134 0H33.0594H18.1905C15.3964 0 12.8173 1.48494 11.4301 3.90772L0.859699 22.2154C0.293079 23.1728 0 24.2865 0 25.4002Z" fill="#6363f1"></path>
<path d="M58.6151 25.4001C58.6151 24.2864 58.322 23.1727 57.7554 22.2153L47.3413 4.18118C45.407 0.840078 41.343 -0.840243 37.4157 0.449306C35.4228 1.11362 33.8206 2.59855 32.7655 4.41565L30.4209 8.46014L40.2097 25.4001L29.8738 43.317L27.3924 47.6155C26.6499 48.8856 25.6535 49.9602 24.4811 50.8003H25.5558H40.4247C43.2187 50.8003 45.7978 49.3154 47.185 46.8926L57.7554 28.5849C58.322 27.6275 58.6151 26.5138 58.6151 25.4001Z" fill="#6363f1"></path>
</svg>

After

Width:  |  Height:  |  Size: 967 B

View File

@@ -0,0 +1,130 @@
import { Account, User, Awaitable } from "."
export interface AdapterUser extends User {
id: string
email: string
emailVerified: Date | null
}
export interface AdapterAccount extends Account {
userId: string
}
export interface AdapterSession {
/** A randomly generated value that is used to get hold of the session. */
sessionToken: string
/** Used to connect the session to a particular user */
userId: string
expires: Date
}
export interface VerificationToken {
identifier: string
expires: Date
token: string
}
/**
* Using a custom adapter you can connect to any database backend or even several different databases.
* Custom adapters created and maintained by our community can be found in the adapters repository.
* Feel free to add a custom adapter from your project to the repository,
* or even become a maintainer of a certain adapter.
* Custom adapters can still be created and used in a project without being added to the repository.
*
* **Required methods**
*
* _(These methods are required for all sign in flows)_
* - `createUser`
* - `getUser`
* - `getUserByEmail`
* - `getUserByAccount`
* - `linkAccount`
* - `createSession`
* - `getSessionAndUser`
* - `updateSession`
* - `deleteSession`
* - `updateUser`
*
* _(Required to support email / passwordless sign in)_
*
* - `createVerificationToken`
* - `useVerificationToken`
*
* **Unimplemented methods**
*
* _(These methods will be required in a future release, but are not yet invoked)_
* - `deleteUser`
* - `unlinkAccount`
*
* [Adapters Overview](https://next-auth.js.org/adapters/overview) |
* [Create a custom adapter](https://next-auth.js.org/tutorials/creating-a-database-adapter)
*/
export type Adapter<WithVerificationToken = boolean> = DefaultAdapter &
(WithVerificationToken extends true
? {
createVerificationToken: (
verificationToken: VerificationToken
) => Awaitable<VerificationToken | null | undefined>
/**
* Return verification token from the database
* and delete it so it cannot be used again.
*/
useVerificationToken: (params: {
identifier: string
token: string
}) => Awaitable<VerificationToken | null>
}
: {})
export interface DefaultAdapter {
createUser: (user: Omit<AdapterUser, "id">) => Awaitable<AdapterUser>
getUser: (id: string) => Awaitable<AdapterUser | null>
getUserByEmail: (email: string) => Awaitable<AdapterUser | null>
/** Using the provider id and the id of the user for a specific account, get the user. */
getUserByAccount: (
providerAccountId: Pick<AdapterAccount, "provider" | "providerAccountId">
) => Awaitable<AdapterUser | null>
updateUser: (user: Partial<AdapterUser> & Pick<AdapterUser, 'id'>) => Awaitable<AdapterUser>
/** @todo Implement */
deleteUser?: (
userId: string
) => Promise<void> | Awaitable<AdapterUser | null | undefined>
linkAccount: (
account: AdapterAccount
) => Promise<void> | Awaitable<AdapterAccount | null | undefined>
/** @todo Implement */
unlinkAccount?: (
providerAccountId: Pick<AdapterAccount, "provider" | "providerAccountId">
) => Promise<void> | Awaitable<AdapterAccount | undefined>
/** Creates a session for the user and returns it. */
createSession: (session: {
sessionToken: string
userId: string
expires: Date
}) => Awaitable<AdapterSession>
getSessionAndUser: (
sessionToken: string
) => Awaitable<{ session: AdapterSession; user: AdapterUser } | null>
updateSession: (
session: Partial<AdapterSession> & Pick<AdapterSession, "sessionToken">
) => Awaitable<AdapterSession | null | undefined>
/**
* Deletes a session from the database.
* It is preferred that this method also returns the session
* that is being deleted for logging purposes.
*/
deleteSession: (
sessionToken: string
) => Promise<void> | Awaitable<AdapterSession | null | undefined>
createVerificationToken?: (
verificationToken: VerificationToken
) => Awaitable<VerificationToken | null | undefined>
/**
* Return verification token from the database
* and delete it so it cannot be used again.
*/
useVerificationToken?: (params: {
identifier: string
token: string
}) => Awaitable<VerificationToken | null>
}

View File

@@ -0,0 +1,188 @@
import { rest } from "msw"
import { render, screen, waitFor } from "@testing-library/react"
import { server, mockSession } from "./helpers/mocks"
import { printFetchCalls } from "./helpers/utils"
import { SessionProvider, useSession, signOut, getSession } from "../../react"
const origDocumentVisibility = document.visibilityState
const fetchSpy = jest.spyOn(global, "fetch")
beforeAll(() => {
server.listen()
})
afterEach(() => {
server.resetHandlers()
changeTabVisibility(origDocumentVisibility)
fetchSpy.mockClear()
})
afterAll(() => {
server.close()
})
test("fetches the session once and re-uses it for different consumers", async () => {
render(<ProviderFlow />)
expect(screen.getByTestId("session-1")).toHaveTextContent("loading")
expect(screen.getByTestId("session-2")).toHaveTextContent("loading")
return waitFor(() => {
expect(fetchSpy).toHaveBeenCalledTimes(1)
expect(fetchSpy).toHaveBeenCalledWith(
"/api/auth/session",
expect.anything()
)
const session1 = screen.getByTestId("session-1").textContent
const session2 = screen.getByTestId("session-2").textContent
expect(session1).toEqual(session2)
})
})
test("when there's an existing session, it won't try to fetch a new one straightaway", async () => {
render(<ProviderFlow session={mockSession} />)
expect(fetchSpy).not.toHaveBeenCalled()
})
test("will refetch the session when the browser tab becomes active again", async () => {
render(<ProviderFlow session={mockSession} />)
expect(fetchSpy).not.toHaveBeenCalled()
// Hide the current tab
changeTabVisibility("hidden")
// Given the current tab got hidden, it should not attempt to re-fetch the session
expect(fetchSpy).not.toHaveBeenCalled()
// Make the tab again visible
changeTabVisibility("visible")
// Given the user made the tab visible again, now attempts to sync and re-fetch the session
return waitFor(() => {
expect(fetchSpy).toHaveBeenCalledTimes(1)
expect(fetchSpy).toHaveBeenCalledWith(
"/api/auth/session",
expect.anything()
)
})
})
test("will refetch the session if told to do so programmatically from another window", async () => {
render(<ProviderFlow session={mockSession} />)
expect(fetchSpy).not.toHaveBeenCalled()
// Hide the current tab
changeTabVisibility("hidden")
// Given the current tab got hidden, it should not attempt to re-fetch the session
expect(fetchSpy).not.toHaveBeenCalled()
// simulate sign-out triggered by another tab
signOut({ redirect: false })
// Given signed out in another tab, it attempts to sync and re-fetch the session
return waitFor(() => {
expect(fetchSpy).toHaveBeenCalledWith(
"/api/auth/session",
expect.anything()
)
// We should have a call to sign-out and a call to refetch the session accordingly
expect(printFetchCalls(fetchSpy.mock.calls)).toMatchInlineSnapshot(`
Array [
"GET /api/auth/csrf",
"POST /api/auth/signout",
"GET /api/auth/session",
]
`)
})
})
test("allows to customize how often the session will be re-fetched through polling", () => {
jest.useFakeTimers()
render(<ProviderFlow session={mockSession} refetchInterval={1} />)
// we provided a mock session so it shouldn't try to fetch a new one
expect(fetchSpy).not.toHaveBeenCalled()
jest.advanceTimersByTime(1000)
expect(fetchSpy).toHaveBeenCalledTimes(1)
expect(fetchSpy).toHaveBeenCalledWith("/api/auth/session", expect.anything())
jest.advanceTimersByTime(1000)
// it should have tried to refetch the session, hence counting 2 calls to the session endpoint
expect(fetchSpy).toHaveBeenCalledTimes(2)
expect(printFetchCalls(fetchSpy.mock.calls)).toMatchInlineSnapshot(`
Array [
"GET /api/auth/session",
"GET /api/auth/session",
]
`)
})
test("allows to customize the URL for session fetching", async () => {
const myPath = "/api/v1/auth"
server.use(
rest.get(`${myPath}/session`, (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockSession))
)
)
render(<ProviderFlow session={mockSession} basePath={myPath} />)
// there's an existing session so it should not try to fetch a new one
expect(fetchSpy).not.toHaveBeenCalled()
// force a session refetch across all clients...
getSession()
return waitFor(() => {
expect(fetchSpy).toHaveBeenCalledTimes(1)
expect(fetchSpy).toHaveBeenCalledWith(
`${myPath}/session`,
expect.anything()
)
})
})
function ProviderFlow(props) {
return (
<SessionProvider {...props}>
<SessionConsumer />
<SessionConsumer testId="2" />
</SessionProvider>
)
}
function SessionConsumer({ testId = 1, ...rest }) {
const { data: session, status } = useSession(rest)
return (
<div data-testid={`session-${testId}`}>
{status === "loading" ? "loading" : JSON.stringify(session)}
</div>
)
}
function changeTabVisibility(status) {
const visibleStates = ["visible", "hidden"]
if (!visibleStates.includes(status)) return
Object.defineProperty(document, "visibilityState", {
configurable: true,
value: status,
})
document.dispatchEvent(new Event("visibilitychange"))
}

View File

@@ -0,0 +1,104 @@
import { useState } from "react"
import userEvent from "@testing-library/user-event"
import { render, screen, waitFor } from "@testing-library/react"
import { server, mockCSRFToken } from "./helpers/mocks"
import logger from "../../utils/logger"
import { getCsrfToken } from "../../react"
import { rest } from "msw"
jest.mock("../../utils/logger", () => ({
__esModule: true,
default: {
warn: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
},
proxyLogger(logger) {
return logger
},
}))
beforeAll(() => {
server.listen()
})
afterEach(() => {
server.resetHandlers()
jest.clearAllMocks()
})
afterAll(() => {
server.close()
})
test("returns the Cross Site Request Forgery Token (CSRF Token) required to make POST requests", async () => {
render(<CSRFFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(screen.getByTestId("csrf-result").textContent).toEqual(
mockCSRFToken.csrfToken
)
})
})
test("when there's no CSRF token returned, it'll reflect that", async () => {
server.use(
rest.get("*/api/auth/csrf", (req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
...mockCSRFToken,
csrfToken: null,
})
)
)
)
render(<CSRFFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(screen.getByTestId("csrf-result").textContent).toBe("null-response")
})
})
test("when the fetch fails it'll throw a client fetch error", async () => {
server.use(
rest.get("*/api/auth/csrf", (req, res, ctx) =>
res(ctx.status(500), ctx.text("some error happened"))
)
)
render(<CSRFFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", {
url: "/api/auth/csrf",
error: new SyntaxError("Unexpected token s in JSON at position 0"),
})
})
})
function CSRFFlow() {
const [response, setResponse] = useState()
async function handleCSRF() {
const result = await getCsrfToken()
setResponse(result)
}
return (
<>
<p data-testid="csrf-result">
{response === null ? "null-response" : response || "no response"}
</p>
<button onClick={handleCSRF}>Get CSRF</button>
</>
)
}

View File

@@ -0,0 +1,90 @@
import { setupServer } from "msw/node"
import { rest } from "msw"
import { randomBytes } from "crypto"
export const mockSession = {
ok: true,
user: {
image: null,
name: "John",
email: "john@email.com",
},
expires: 123213139,
}
export const mockProviders = {
ok: true,
github: {
id: "github",
name: "Github",
type: "oauth",
signinUrl: "path/to/signin",
callbackUrl: "path/to/callback",
},
credentials: {
id: "credentials",
name: "Credentials",
type: "credentials",
authorize: null,
credentials: null,
},
email: {
id: "email",
type: "email",
name: "Email",
},
}
export const mockCSRFToken = {
ok: true,
csrfToken: randomBytes(32).toString("hex"),
}
export const mockGithubResponse = {
ok: true,
status: 200,
url: "https://path/to/github/url",
}
export const mockCredentialsResponse = {
ok: true,
status: 200,
url: "https://path/to/credentials/url",
}
export const mockEmailResponse = {
ok: true,
status: 200,
url: "https://path/to/email/url",
}
export const mockSignOutResponse = {
ok: true,
status: 200,
url: "https://path/to/signout/url",
}
export const server = setupServer(
rest.post("*/api/auth/signout", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockSignOutResponse))
),
rest.get("*/api/auth/session", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockSession))
),
rest.get("*/api/auth/csrf", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockCSRFToken))
),
rest.get("*/api/auth/providers", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockProviders))
),
rest.post("*/api/auth/signin/github", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockGithubResponse))
),
rest.post("*/api/auth/callback/credentials", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockCredentialsResponse))
),
rest.post("*/api/auth/signin/email", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockEmailResponse))
),
rest.post("*/api/auth/_log", (req, res, ctx) => res(ctx.status(200)))
)

View File

@@ -0,0 +1,14 @@
export function getBroadcastEvents() {
return window.localStorage.setItem.mock.calls
.filter((call) => call[0] === "nextauth.message")
.map(([eventName, value]) => {
const { timestamp, ...rest } = JSON.parse(value)
return { eventName, value: rest }
})
}
export function printFetchCalls(mockCalls) {
return mockCalls.map(([path, { method = "GET" }]) => {
return `${method.toUpperCase()} ${path}`
})
}

View File

@@ -0,0 +1,84 @@
import { useState } from "react"
import userEvent from "@testing-library/user-event"
import { render, screen, waitFor } from "@testing-library/react"
import { server, mockProviders } from "./helpers/mocks"
import { getProviders } from "../../react"
import logger from "../../utils/logger"
import { rest } from "msw"
jest.mock("../../utils/logger", () => ({
__esModule: true,
default: {
warn: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
},
proxyLogger(logger) {
return logger
},
}))
beforeAll(() => {
server.listen()
})
afterEach(() => {
server.resetHandlers()
jest.clearAllMocks()
})
afterAll(() => {
server.close()
})
test("when called it'll return the currently configured providers for sign in", async () => {
render(<ProvidersFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(screen.getByTestId("providers-result").textContent).toEqual(
JSON.stringify(mockProviders)
)
})
})
test("when failing to fetch the providers, it'll log the error", async () => {
server.use(
rest.get("*/api/auth/providers", (req, res, ctx) =>
res(ctx.status(500), ctx.text("some error happened"))
)
)
render(<ProvidersFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", {
url: "/api/auth/providers",
error: new SyntaxError("Unexpected token s in JSON at position 0"),
})
})
})
function ProvidersFlow() {
const [response, setResponse] = useState()
async function handleGerProviders() {
const result = await getProviders()
setResponse(result)
}
return (
<>
<p data-testid="providers-result">
{response === null
? "null-response"
: JSON.stringify(response) || "no response"}
</p>
<button onClick={handleGerProviders}>Get Providers</button>
</>
)
}

View File

@@ -0,0 +1,97 @@
import { render, screen, waitFor } from "@testing-library/react"
import { rest } from "msw"
import { server, mockSession } from "./helpers/mocks"
import logger from "../../utils/logger"
import { useState, useEffect } from "react"
import { getSession } from "../../react"
import { getBroadcastEvents } from "./helpers/utils"
jest.mock("../../utils/logger", () => ({
__esModule: true,
default: {
warn: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
},
proxyLogger(logger) {
return logger
},
}))
beforeAll(() => server.listen())
beforeEach(() => {
// eslint-disable-next-line no-proto
jest.spyOn(window.localStorage.__proto__, "setItem")
})
afterEach(() => {
server.resetHandlers()
jest.clearAllMocks()
})
afterAll(() => {
server.close()
})
test("if it can fetch the session, it should store it in `localStorage`", async () => {
render(<SessionFlow />)
// In the start, there is no session
const noSession = await screen.findByText("No session")
expect(noSession).toBeInTheDocument()
// After we fetched the session, it should have been rendered by `<SessionFlow />`
const session = await screen.findByText(new RegExp(mockSession.user.name))
expect(session).toBeInTheDocument()
const broadcastCalls = getBroadcastEvents()
const [broadcastedEvent] = broadcastCalls
expect(broadcastCalls).toHaveLength(1)
expect(broadcastCalls).toHaveLength(1)
expect(broadcastedEvent.eventName).toBe("nextauth.message")
expect(broadcastedEvent.value).toStrictEqual({
data: {
trigger: "getSession",
},
event: "session",
})
})
test("if there's an error fetching the session, it should log it", async () => {
server.use(
rest.get("*/api/auth/session", (req, res, ctx) => {
return res(ctx.status(500), ctx.body("Server error"))
})
)
render(<SessionFlow />)
await waitFor(() => {
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", {
url: "/api/auth/session",
error: new SyntaxError("Unexpected token S in JSON at position 0"),
})
})
})
function SessionFlow() {
const [session, setSession] = useState(null)
useEffect(() => {
async function fetchUserSession() {
try {
const result = await getSession()
setSession(result)
} catch (e) {
console.error(e)
}
}
fetchUserSession()
}, [])
if (session) return <pre>{JSON.stringify(session, null, 2)}</pre>
return <p>No session</p>
}

View File

@@ -0,0 +1,290 @@
import { useState } from "react"
import userEvent from "@testing-library/user-event"
import { render, screen, waitFor } from "@testing-library/react"
import logger from "../../utils/logger"
import {
server,
mockCredentialsResponse,
mockEmailResponse,
mockGithubResponse,
} from "./helpers/mocks"
import { signIn } from "../../react"
import { rest } from "msw"
const { location } = window
jest.mock("../../utils/logger", () => ({
__esModule: true,
default: {
warn: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
},
proxyLogger(logger) {
return logger
},
}))
beforeAll(() => {
server.listen()
let _href = window.location.href
// Allows to mutate `window.location`...
delete window.location
window.location = {
reload: jest.fn(),
}
Object.defineProperty(window.location, "href", {
get: () => _href,
// whatwg-fetch or whatwg-url does not seem to work with relative URLs
set: (href) => {
_href = href.startsWith("/") ? `http://localhost${href}` : href
return _href
},
})
})
beforeEach(() => {
jest.clearAllMocks()
server.resetHandlers()
})
afterAll(() => {
window.location = location
server.close()
})
const callbackUrl = "https://redirects/to"
test.each`
provider | type
${""} | ${"no"}
${"foo"} | ${"unknown"}
`(
"if $type provider, it redirects to the default sign-in page",
async ({ provider }) => {
render(<SignInFlow providerId={provider} callbackUrl={callbackUrl} />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.href).toBe(
`http://localhost/api/auth/signin?${new URLSearchParams({
callbackUrl,
})}`
)
})
}
)
test.each`
provider | type
${""} | ${"no"}
${"foo"} | ${"unknown"}
`(
"if $type provider supplied and no callback URL, redirects using the current location",
async ({ provider }) => {
render(<SignInFlow providerId={provider} />)
const callbackUrl = window.location.href
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.href).toBe(
`http://localhost/api/auth/signin?${new URLSearchParams({
callbackUrl,
})}`
)
})
}
)
test.each`
provider | mockUrl
${`email`} | ${mockEmailResponse.url}
${`credentials`} | ${mockCredentialsResponse.url}
`(
"$provider provider redirects if `redirect` is `true`",
async ({ provider, mockUrl }) => {
render(<SignInFlow providerId={provider} redirect={true} />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.href).toBe(mockUrl)
})
}
)
test("redirection can't be stopped using an oauth provider", async () => {
render(
<SignInFlow
providerId="github"
callbackUrl={callbackUrl}
redirect={false}
/>
)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.href).toBe(mockGithubResponse.url)
})
})
test("redirection can be stopped using the 'credentials' provider", async () => {
render(
<SignInFlow
providerId="credentials"
callbackUrl={callbackUrl}
redirect={false}
/>
)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.href).not.toBe(mockCredentialsResponse.url)
expect(screen.getByTestId("signin-result").textContent).not.toBe(
"no response"
)
})
// snapshot the expected return shape from `signIn`
expect(JSON.parse(screen.getByTestId("signin-result").textContent))
.toMatchInlineSnapshot(`
Object {
"error": null,
"ok": true,
"status": 200,
"url": "https://path/to/credentials/url",
}
`)
})
test("redirection can be stopped using the 'email' provider", async () => {
render(
<SignInFlow providerId="email" callbackUrl={callbackUrl} redirect={false} />
)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.href).not.toBe(mockEmailResponse.url)
expect(screen.getByTestId("signin-result").textContent).not.toBe(
"no response"
)
})
// snapshot the expected return shape from `signIn` oauth
expect(JSON.parse(screen.getByTestId("signin-result").textContent))
.toMatchInlineSnapshot(`
Object {
"error": null,
"ok": true,
"status": 200,
"url": "https://path/to/email/url",
}
`)
})
test("if callback URL contains a hash we force a window reload when re-directing", async () => {
const mockUrlWithHash = "https://path/to/email/url#foo-bar-baz"
server.use(
rest.post("*/api/auth/signin/email", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
...mockEmailResponse,
url: mockUrlWithHash,
})
)
})
)
render(<SignInFlow providerId="email" callbackUrl={mockUrlWithHash} />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.href).toBe(mockUrlWithHash)
// the browser will not refresh the page if the redirect URL contains a hash, hence we force it on the client, see #1289
expect(window.location.reload).toHaveBeenCalledTimes(1)
})
})
test("params are propagated to the signin URL when supplied", async () => {
let matchedParams = ""
const authParams = "foo=bar&bar=foo"
server.use(
rest.post("*/auth/signin/github", (req, res, ctx) => {
matchedParams = req.url.search
return res(ctx.status(200), ctx.json(mockGithubResponse))
})
)
render(<SignInFlow providerId="github" authorizationParams={authParams} />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(matchedParams).toEqual(`?${authParams}`)
})
})
test("when it fails to fetch the providers, it redirected back to signin page", async () => {
const errorMsg = "Error when retrieving providers"
server.use(
rest.get("*/api/auth/providers", (req, res, ctx) =>
res(ctx.status(500), ctx.json(errorMsg))
)
)
render(<SignInFlow providerId="github" />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.href).toBe(`http://localhost/api/auth/error`)
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", {
error: "Error when retrieving providers",
url: "/api/auth/providers",
})
})
})
function SignInFlow({
providerId,
callbackUrl,
redirect = true,
authorizationParams = {},
}) {
const [response, setResponse] = useState(null)
async function handleSignIn() {
const result = await signIn(
providerId,
{ callbackUrl, redirect },
authorizationParams
)
setResponse(result)
}
return (
<>
<p data-testid="signin-result">
{response ? JSON.stringify(response) : "no response"}
</p>
<button onClick={handleSignIn}>Sign in</button>
</>
)
}

View File

@@ -0,0 +1,124 @@
import { useState } from "react"
import userEvent from "@testing-library/user-event"
import { render, screen, waitFor } from "@testing-library/react"
import { server, mockSignOutResponse } from "./helpers/mocks"
import { signOut } from "../../react"
import { rest } from "msw"
import { getBroadcastEvents } from "./helpers/utils"
const { location } = window
beforeAll(() => {
server.listen()
// Allows to mutate `window.location`...
delete window.location
window.location = {
reload: jest.fn(),
href: location.href,
}
})
beforeEach(() => {
// eslint-disable-next-line no-proto
jest.spyOn(window.localStorage.__proto__, "setItem")
})
afterEach(() => {
jest.clearAllMocks()
server.resetHandlers()
})
afterAll(() => {
window.location = location
server.close()
})
const callbackUrl = "https://redirects/to"
test("by default it redirects to the current URL if the server did not provide one", async () => {
server.use(
rest.post("*/api/auth/signout", (req, res, ctx) =>
res(ctx.status(200), ctx.json({ ...mockSignOutResponse, url: undefined }))
)
)
render(<SignOutFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.href).toBe(window.location.href)
})
})
test("it redirects to the URL allowed by the server", async () => {
render(<SignOutFlow callbackUrl={callbackUrl} />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.href).toBe(mockSignOutResponse.url)
})
})
test("if url contains a hash during redirection a page reload happens", async () => {
const mockUrlWithHash = "https://path/to/email/url#foo-bar-baz"
server.use(
rest.post("*/api/auth/signout", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
...mockSignOutResponse,
url: mockUrlWithHash,
})
)
})
)
render(<SignOutFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.href).toBe(mockUrlWithHash)
})
})
test("will broadcast the signout event to other tabs", async () => {
render(<SignOutFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
const broadcastCalls = getBroadcastEvents()
const [broadcastedEvent] = broadcastCalls
expect(broadcastCalls).toHaveLength(1)
expect(broadcastedEvent.eventName).toBe("nextauth.message")
expect(broadcastedEvent.value).toStrictEqual({
data: {
trigger: "signout",
},
event: "session",
})
})
})
function SignOutFlow({ callbackUrl, redirect = true }) {
const [response, setResponse] = useState(null)
async function handleSignOut() {
const result = await signOut({ callbackUrl, redirect })
setResponse(result)
}
return (
<>
<p data-testid="signout-result">
{response ? JSON.stringify(response) : "no response"}
</p>
<button onClick={handleSignOut}>Sign out</button>
</>
)
}

View File

@@ -0,0 +1,140 @@
import { rest } from "msw"
import { renderHook } from "@testing-library/react-hooks"
import { render, waitFor } from "@testing-library/react"
import { SessionProvider, useSession, signOut } from "../../react"
import { server, mockSession } from "./helpers/mocks"
const origConsoleError = console.error
const { location } = window
let _href = window.location.href
beforeAll(() => {
// Prevent noise on the terminal... `next-auth` will log to `console.error`
// every time a request fails, which makes the tests output very noisy...
console.error = jest.fn()
// Allows to mutate `window.location`...
delete window.location
window.location = {}
Object.defineProperty(window.location, "href", {
get: () => _href,
// whatwg-fetch or whatwg-url does not seem to work with relative URLs
set: (href) => {
_href = href.startsWith("/") ? `http://localhost${href}` : href
return _href
},
})
server.listen()
})
afterEach(() => {
server.resetHandlers()
_href = "http://localhost/"
// clear the internal session cache...
signOut({ redirect: false })
})
afterAll(() => {
console.error = origConsoleError
window.location = location
server.close()
})
test("it won't allow to fetch the session in isolation without a session context", () => {
function App() {
useSession()
return null
}
expect(() => render(<App />)).toThrow(
"[next-auth]: `useSession` must be wrapped in a <SessionProvider />"
)
})
test("when fetching the session, there won't be `data` and `status` will be 'loading'", () => {
const { result } = renderHook(() => useSession(), {
wrapper: SessionProvider,
})
expect(result.current.data).toBe(undefined)
expect(result.current.status).toBe("loading")
})
test("when session is fetched, `data` will contain the session data and `status` will be 'authenticated'", async () => {
const { result } = renderHook(() => useSession(), {
wrapper: SessionProvider,
})
await waitFor(() => {
expect(result.current.data).toEqual(mockSession)
expect(result.current.status).toBe("authenticated")
})
})
test("when it fails to fetch the session, `data` will be null and `status` will be 'unauthenticated'", async () => {
server.use(
rest.get(`http://localhost/api/auth/session`, (_, res, ctx) =>
res(ctx.status(401), ctx.json({}))
)
)
const { result } = renderHook(() => useSession(), {
wrapper: SessionProvider,
})
return waitFor(() => {
expect(result.current.data).toEqual(null)
expect(result.current.status).toBe("unauthenticated")
})
})
test("it'll redirect to sign-in page if the session is required and the user is not authenticated", async () => {
server.use(
rest.get(`http://localhost/api/auth/session`, (req, res, ctx) =>
res(ctx.status(401), ctx.json({}))
)
)
const callbackUrl = window.location.href
const { result } = renderHook(() => useSession({ required: true }), {
wrapper: SessionProvider,
})
await waitFor(() => {
expect(result.current.data).toEqual(null)
expect(result.current.status).toBe("loading")
})
expect(window.location.href).toBe(
`http://localhost/api/auth/signin?${new URLSearchParams({
error: "SessionRequired",
callbackUrl,
})}`
)
})
test("will call custom redirect logic if supplied when the user could not authenticate", async () => {
server.use(
rest.get(`http://localhost/api/auth/session`, (_, res, ctx) =>
res(ctx.status(401), ctx.json({}))
)
)
const customRedirect = jest.fn()
const { result } = renderHook(
() => useSession({ required: true, onUnauthenticated: customRedirect }),
{
wrapper: SessionProvider,
}
)
await waitFor(() => {
expect(result.current.data).toEqual(null)
expect(result.current.status).toBe("loading")
})
expect(customRedirect).toHaveBeenCalledTimes(1)
})

View File

@@ -0,0 +1,120 @@
import type { IncomingMessage } from "http"
import type { LoggerInstance, Session } from ".."
export interface AuthClientConfig {
baseUrl: string
basePath: string
baseUrlServer: string
basePathServer: string
/** Stores last session response */
_session?: Session | null | undefined
/** Used for timestamp since last sycned (in seconds) */
_lastSync: number
/**
* Stores the `SessionProvider`'s session update method to be able to
* trigger session updates from places like `signIn` or `signOut`
*/
_getSession: (...args: any[]) => any
}
export interface CtxOrReq {
req?: Partial<IncomingMessage> & { body?: any }
ctx?: { req: Partial<IncomingMessage> & { body?: any } }
}
/**
* If passed 'appContext' via getInitialProps() in _app.js
* then get the req object from ctx and use that for the
* req value to allow `fetchData` to
* work seemlessly in getInitialProps() on server side
* pages *and* in _app.js.
*/
export async function fetchData<T = any>(
path: string,
__NEXTAUTH: AuthClientConfig,
logger: LoggerInstance,
{ ctx, req = ctx?.req }: CtxOrReq = {}
): Promise<T | null> {
const url = `${apiBaseUrl(__NEXTAUTH)}/${path}`
try {
const options: RequestInit = {
headers: {
"Content-Type": "application/json",
...(req?.headers?.cookie ? { cookie: req.headers.cookie } : {}),
},
}
if (req?.body) {
options.body = JSON.stringify(req.body)
options.method = "POST"
}
const res = await fetch(url, options)
const data = await res.json()
if (!res.ok) throw data
return Object.keys(data).length > 0 ? data : null // Return null if data empty
} catch (error) {
logger.error("CLIENT_FETCH_ERROR", { error: error as Error, url })
return null
}
}
export function apiBaseUrl(__NEXTAUTH: AuthClientConfig) {
if (typeof window === "undefined") {
// Return absolute path when called server side
return `${__NEXTAUTH.baseUrlServer}${__NEXTAUTH.basePathServer}`
}
// Return relative path when called client side
return __NEXTAUTH.basePath
}
/** Returns the number of seconds elapsed since January 1, 1970 00:00:00 UTC. */
export function now() {
return Math.floor(Date.now() / 1000)
}
export interface BroadcastMessage {
event?: "session"
data?: { trigger?: "signout" | "getSession" }
clientId: string
timestamp: number
}
/**
* Inspired by [Broadcast Channel API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API)
* Only not using it directly, because Safari does not support it.
*
* https://caniuse.com/?search=broadcastchannel
*/
export function BroadcastChannel(name = "nextauth.message") {
return {
/** Get notified by other tabs/windows. */
receive(onReceive: (message: BroadcastMessage) => void) {
const handler = (event: StorageEvent) => {
if (event.key !== name) return
const message: BroadcastMessage = JSON.parse(event.newValue ?? "{}")
if (message?.event !== "session" || !message?.data) return
onReceive(message)
}
window.addEventListener("storage", handler)
return () => window.removeEventListener("storage", handler)
},
/** Notify other tabs/windows. */
post(message: Record<string, unknown>) {
if (typeof window === "undefined") return
try {
localStorage.setItem(
name,
JSON.stringify({ ...message, timestamp: now() })
)
} catch {
/**
* The localStorage API isn't always available.
* It won't work in private mode prior to Safari 11 for example.
* Notifications are simply dropped if an error is encountered.
*/
}
},
}
}

View File

@@ -0,0 +1,127 @@
import type { EventCallbacks, LoggerInstance } from ".."
/**
* Same as the default `Error`, but it is JSON serializable.
* @source https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
*/
export class UnknownError extends Error {
code: string
constructor(error: Error | string) {
// Support passing error or string
super((error as Error)?.message ?? error)
this.name = "UnknownError"
this.code = (error as any).code
if (error instanceof Error) {
this.stack = error.stack
}
}
toJSON() {
return {
name: this.name,
message: this.message,
stack: this.stack,
}
}
}
export class OAuthCallbackError extends UnknownError {
name = "OAuthCallbackError"
}
/**
* Thrown when an Email address is already associated with an account
* but the user is trying an OAuth account that is not linked to it.
*/
export class AccountNotLinkedError extends UnknownError {
name = "AccountNotLinkedError"
}
export class MissingAPIRoute extends UnknownError {
name = "MissingAPIRouteError"
code = "MISSING_NEXTAUTH_API_ROUTE_ERROR"
}
export class MissingSecret extends UnknownError {
name = "MissingSecretError"
code = "NO_SECRET"
}
export class MissingAuthorize extends UnknownError {
name = "MissingAuthorizeError"
code = "CALLBACK_CREDENTIALS_HANDLER_ERROR"
}
export class MissingAdapter extends UnknownError {
name = "MissingAdapterError"
code = "EMAIL_REQUIRES_ADAPTER_ERROR"
}
export class MissingAdapterMethods extends UnknownError {
name = "MissingAdapterMethodsError"
code = "MISSING_ADAPTER_METHODS_ERROR"
}
export class UnsupportedStrategy extends UnknownError {
name = "UnsupportedStrategyError"
code = "CALLBACK_CREDENTIALS_JWT_ERROR"
}
export class InvalidCallbackUrl extends UnknownError {
name = "InvalidCallbackUrl"
code = "INVALID_CALLBACK_URL_ERROR"
}
type Method = (...args: any[]) => Promise<any>
export function upperSnake(s: string) {
return s.replace(/([A-Z])/g, "_$1").toUpperCase()
}
export function capitalize(s: string) {
return `${s[0].toUpperCase()}${s.slice(1)}`
}
/**
* Wraps an object of methods and adds error handling.
*/
export function eventsErrorHandler(
methods: Partial<EventCallbacks>,
logger: LoggerInstance
): Partial<EventCallbacks> {
return Object.keys(methods).reduce<any>((acc, name) => {
acc[name] = async (...args: any[]) => {
try {
const method: Method = methods[name as keyof Method]
return await method(...args)
} catch (e) {
logger.error(`${upperSnake(name)}_EVENT_ERROR`, e as Error)
}
}
return acc
}, {})
}
/** Handles adapter induced errors. */
export function adapterErrorHandler<TAdapter>(
adapter: TAdapter | undefined,
logger: LoggerInstance
): TAdapter | undefined {
if (!adapter) return
return Object.keys(adapter).reduce<any>((acc, name) => {
acc[name] = async (...args: any[]) => {
try {
logger.debug(`adapter_${name}`, { args })
const method: Method = adapter[name as keyof Method]
return await method(...args)
} catch (error) {
logger.error(`adapter_error_${name}`, error as Error)
const e = new UnknownError(error as Error)
e.name = `${capitalize(name)}Error`
throw e
}
}
return acc
}, {})
}

View File

@@ -0,0 +1,322 @@
import logger, { setLogger } from "../utils/logger"
import { detectOrigin } from "../utils/detect-origin"
import * as routes from "./routes"
import renderPage from "./pages"
import { init } from "./init"
import { assertConfig } from "./lib/assert"
import { SessionStore } from "./lib/cookie"
import type { AuthAction, AuthOptions } from "./types"
import type { Cookie } from "./lib/cookie"
import type { ErrorType } from "./pages/error"
import { parse as parseCookie } from "cookie"
export interface RequestInternal {
/** @default "http://localhost:3000" */
origin?: string
method?: string
cookies?: Partial<Record<string, string>>
headers?: Record<string, any>
query?: Record<string, any>
body?: Record<string, any>
action: AuthAction
providerId?: string
error?: string
}
export interface NextAuthHeader {
key: string
value: string
}
export interface ResponseInternal<
Body extends string | Record<string, any> | any[] = any
> {
status?: number
headers?: NextAuthHeader[]
body?: Body
redirect?: string
cookies?: Cookie[]
}
export interface NextAuthHandlerParams {
req: Request | RequestInternal
options: AuthOptions
}
async function getBody(req: Request): Promise<Record<string, any> | undefined> {
try {
return await req.json()
} catch {}
}
// TODO:
async function toInternalRequest(
req: RequestInternal | Request
): Promise<RequestInternal> {
if (req instanceof Request) {
const url = new URL(req.url)
// TODO: handle custom paths?
const nextauth = url.pathname.split("/").slice(3)
const headers = Object.fromEntries(req.headers)
const query: Record<string, any> = Object.fromEntries(url.searchParams)
query.nextauth = nextauth
return {
action: nextauth[0] as AuthAction,
method: req.method,
headers,
body: await getBody(req),
cookies: parseCookie(req.headers.get("cookie") ?? ""),
providerId: nextauth[1],
error: url.searchParams.get("error") ?? nextauth[1],
origin: detectOrigin(
headers["x-forwarded-host"] ?? headers.host,
headers["x-forwarded-proto"]
),
query,
}
}
const { headers } = req
const host = headers?.["x-forwarded-host"] ?? headers?.host
req.origin = detectOrigin(host, headers?.["x-forwarded-proto"])
return req
}
export async function AuthHandler<
Body extends string | Record<string, any> | any[]
>(params: NextAuthHandlerParams): Promise<ResponseInternal<Body>> {
const { options: authOptions, req: incomingRequest } = params
const req = await toInternalRequest(incomingRequest)
setLogger(authOptions.logger, authOptions.debug)
const assertionResult = assertConfig({ options: authOptions, req })
if (Array.isArray(assertionResult)) {
assertionResult.forEach(logger.warn)
} else if (assertionResult instanceof Error) {
// Bail out early if there's an error in the user config
logger.error(assertionResult.code, assertionResult)
const htmlPages = ["signin", "signout", "error", "verify-request"]
if (!htmlPages.includes(req.action) || req.method !== "GET") {
const message = `There is a problem with the server configuration. Check the server logs for more information.`
return {
status: 500,
headers: [{ key: "Content-Type", value: "application/json" }],
body: { message } as any,
}
}
const { pages, theme } = authOptions
const authOnErrorPage =
pages?.error && req.query?.callbackUrl?.startsWith(pages.error)
if (!pages?.error || authOnErrorPage) {
if (authOnErrorPage) {
logger.error(
"AUTH_ON_ERROR_PAGE_ERROR",
new Error(
`The error page ${pages?.error} should not require authentication`
)
)
}
const render = renderPage({ theme })
return render.error({ error: "configuration" })
}
return {
redirect: `${pages.error}?error=Configuration`,
}
}
const { action, providerId, error, method = "GET" } = req
const { options, cookies } = await init({
authOptions,
action,
providerId,
origin: req.origin,
callbackUrl: req.body?.callbackUrl ?? req.query?.callbackUrl,
csrfToken: req.body?.csrfToken,
cookies: req.cookies,
isPost: method === "POST",
})
const sessionStore = new SessionStore(
options.cookies.sessionToken,
req,
options.logger
)
if (method === "GET") {
const render = renderPage({ ...options, query: req.query, cookies })
const { pages } = options
switch (action) {
case "providers":
return (await routes.providers(options.providers)) as any
case "session": {
const session = await routes.session({ options, sessionStore })
if (session.cookies) cookies.push(...session.cookies)
return { ...session, cookies } as any
}
case "csrf":
return {
headers: [{ key: "Content-Type", value: "application/json" }],
body: { csrfToken: options.csrfToken } as any,
cookies,
}
case "signin":
if (pages.signIn) {
let signinUrl = `${pages.signIn}${
pages.signIn.includes("?") ? "&" : "?"
}callbackUrl=${encodeURIComponent(options.callbackUrl)}`
if (error)
signinUrl = `${signinUrl}&error=${encodeURIComponent(error)}`
return { redirect: signinUrl, cookies }
}
return render.signin()
case "signout":
if (pages.signOut) return { redirect: pages.signOut, cookies }
return render.signout()
case "callback":
if (options.provider) {
const callback = await routes.callback({
body: req.body,
query: req.query,
headers: req.headers,
cookies: req.cookies,
method,
options,
sessionStore,
})
if (callback.cookies) cookies.push(...callback.cookies)
return { ...callback, cookies }
}
break
case "verify-request":
if (pages.verifyRequest) {
return { redirect: pages.verifyRequest, cookies }
}
return render.verifyRequest()
case "error":
// These error messages are displayed in line on the sign in page
if (
[
"Signin",
"OAuthSignin",
"OAuthCallback",
"OAuthCreateAccount",
"EmailCreateAccount",
"Callback",
"OAuthAccountNotLinked",
"EmailSignin",
"CredentialsSignin",
"SessionRequired",
].includes(error as string)
) {
return { redirect: `${options.url}/signin?error=${error}`, cookies }
}
if (pages.error) {
return {
redirect: `${pages.error}${
pages.error.includes("?") ? "&" : "?"
}error=${error}`,
cookies,
}
}
return render.error({ error: error as ErrorType })
default:
}
} else if (method === "POST") {
switch (action) {
case "signin":
// Verified CSRF Token required for all sign-in routes
if (options.csrfTokenVerified && options.provider) {
const signin = await routes.signin({
query: req.query,
body: req.body,
options,
})
if (signin.cookies) cookies.push(...signin.cookies)
return { ...signin, cookies }
}
return { redirect: `${options.url}/signin?csrf=true`, cookies }
case "signout":
// Verified CSRF Token required for signout
if (options.csrfTokenVerified) {
const signout = await routes.signout({ options, sessionStore })
if (signout.cookies) cookies.push(...signout.cookies)
return { ...signout, cookies }
}
return { redirect: `${options.url}/signout?csrf=true`, cookies }
case "callback":
if (options.provider) {
// Verified CSRF Token required for credentials providers only
if (
options.provider.type === "credentials" &&
!options.csrfTokenVerified
) {
return { redirect: `${options.url}/signin?csrf=true`, cookies }
}
const callback = await routes.callback({
body: req.body,
query: req.query,
headers: req.headers,
cookies: req.cookies,
method,
options,
sessionStore,
})
if (callback.cookies) cookies.push(...callback.cookies)
return { ...callback, cookies }
}
break
case "_log": {
if (authOptions.logger) {
try {
const { code, level, ...metadata } = req.body ?? {}
logger[level](code, metadata)
} catch (error) {
// If logging itself failed...
logger.error("LOGGER_ERROR", error as Error)
}
}
return {}
}
case "session": {
// Verified CSRF Token required for session updates
if (options.csrfTokenVerified) {
const session = await routes.session({
options,
sessionStore,
newSession: req.body?.data,
isUpdate: true,
})
if (session.cookies) cookies.push(...session.cookies)
return { ...session, cookies } as any
}
// If CSRF token is invalid, return a 400 status code
// we should not redirect to a page as this is an API route
return { status: 400, body: {} as any, cookies }
}
default:
}
}
return {
status: 400,
body: `Error: This action with HTTP ${method} is not supported by NextAuth.js` as any,
}
}

View File

@@ -0,0 +1,155 @@
import { randomBytes, randomUUID } from "crypto"
import { AuthOptions } from ".."
import logger from "../utils/logger"
import { adapterErrorHandler, eventsErrorHandler } from "./errors"
import parseProviders from "./lib/providers"
import { createSecret } from "./lib/utils"
import * as cookie from "./lib/cookie"
import * as jwt from "../jwt"
import { defaultCallbacks } from "./lib/default-callbacks"
import { createCSRFToken } from "./lib/csrf-token"
import { createCallbackUrl } from "./lib/callback-url"
import { RequestInternal } from "."
import type { InternalOptions } from "./types"
import parseUrl from "../utils/parse-url"
interface InitParams {
origin?: string
authOptions: AuthOptions
providerId?: string
action: InternalOptions["action"]
/** Callback URL value extracted from the incoming request. */
callbackUrl?: string
/** CSRF token value extracted from the incoming request. From body if POST, from query if GET */
csrfToken?: string
/** Is the incoming request a POST request? */
isPost: boolean
cookies: RequestInternal["cookies"]
}
/** Initialize all internal options and cookies. */
export async function init({
authOptions,
providerId,
action,
origin,
cookies: reqCookies,
callbackUrl: reqCallbackUrl,
csrfToken: reqCsrfToken,
isPost,
}: InitParams): Promise<{
options: InternalOptions
cookies: cookie.Cookie[]
}> {
const url = parseUrl(origin)
const secret = createSecret({ authOptions, url })
const { providers, provider } = parseProviders({
providers: authOptions.providers,
url,
providerId,
})
const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle by default
// User provided options are overriden by other options,
// except for the options with special handling above
const options: InternalOptions = {
debug: false,
pages: {},
theme: {
colorScheme: "auto",
logo: "",
brandColor: "",
buttonText: "",
},
// Custom options override defaults
...authOptions,
// These computed settings can have values in authOptions but we override them
// and are request-specific.
url,
action,
// @ts-expect-errors
provider,
cookies: {
...cookie.defaultCookies(
authOptions.useSecureCookies ?? url.base.startsWith("https://")
),
// Allow user cookie options to override any cookie settings above
...authOptions.cookies,
},
secret,
providers,
// Session options
session: {
// If no adapter specified, force use of JSON Web Tokens (stateless)
strategy: authOptions.adapter ? "database" : "jwt",
maxAge,
updateAge: 24 * 60 * 60,
generateSessionToken: () => {
// Use `randomUUID` if available. (Node 15.6+)
return randomUUID?.() ?? randomBytes(32).toString("hex")
},
...authOptions.session,
},
// JWT options
jwt: {
secret, // Use application secret if no keys specified
maxAge, // same as session maxAge,
encode: jwt.encode,
decode: jwt.decode,
...authOptions.jwt,
},
// Event messages
events: eventsErrorHandler(authOptions.events ?? {}, logger),
adapter: adapterErrorHandler(authOptions.adapter, logger),
// Callback functions
callbacks: { ...defaultCallbacks, ...authOptions.callbacks },
logger,
callbackUrl: url.origin,
}
// Init cookies
const cookies: cookie.Cookie[] = []
const {
csrfToken,
cookie: csrfCookie,
csrfTokenVerified,
} = createCSRFToken({
options,
cookieValue: reqCookies?.[options.cookies.csrfToken.name],
isPost,
bodyValue: reqCsrfToken,
})
options.csrfToken = csrfToken
options.csrfTokenVerified = csrfTokenVerified
if (csrfCookie) {
cookies.push({
name: options.cookies.csrfToken.name,
value: csrfCookie,
options: options.cookies.csrfToken.options,
})
}
const { callbackUrl, callbackUrlCookie } = await createCallbackUrl({
options,
cookieValue: reqCookies?.[options.cookies.callbackUrl.name],
paramValue: reqCallbackUrl,
})
options.callbackUrl = callbackUrl
if (callbackUrlCookie) {
cookies.push({
name: options.cookies.callbackUrl.name,
value: callbackUrlCookie,
options: options.cookies.callbackUrl.options,
})
}
return { options, cookies }
}

View File

@@ -0,0 +1,149 @@
import {
MissingAdapter,
MissingAPIRoute,
MissingAuthorize,
MissingSecret,
UnsupportedStrategy,
InvalidCallbackUrl,
MissingAdapterMethods,
} from "../errors"
import parseUrl from "../../utils/parse-url"
import { defaultCookies } from "./cookie"
import type { RequestInternal } from ".."
import type { WarningCode } from "../../utils/logger"
import type { AuthOptions } from "../types"
type ConfigError =
| MissingAPIRoute
| MissingSecret
| UnsupportedStrategy
| MissingAuthorize
| MissingAdapter
let warned = false
function isValidHttpUrl(url: string, baseUrl: string) {
try {
return /^https?:/.test(
new URL(url, url.startsWith("/") ? baseUrl : undefined).protocol
)
} catch {
return false
}
}
/**
* Verify that the user configured `next-auth` correctly.
* Good place to mention deprecations as well.
*
* REVIEW: Make some of these and corresponding docs less Next.js specific?
*/
export function assertConfig(params: {
options: AuthOptions
req: RequestInternal
}): ConfigError | WarningCode[] {
const { options, req } = params
const warnings: WarningCode[] = []
if (!warned) {
if (!req.origin) warnings.push("NEXTAUTH_URL")
// TODO: Make this throw an error in next major. This will also get rid of `NODE_ENV`
if (!options.secret && process.env.NODE_ENV !== "production")
warnings.push("NO_SECRET")
if (options.debug) warnings.push("DEBUG_ENABLED")
}
if (!options.secret && process.env.NODE_ENV === "production") {
return new MissingSecret("Please define a `secret` in production.")
}
// req.query isn't defined when asserting `getServerSession` for example
if (!req.query?.nextauth && !req.action) {
return new MissingAPIRoute(
"Cannot find [...nextauth].{js,ts} in `/pages/api/auth`. Make sure the filename is written correctly."
)
}
const callbackUrlParam = req.query?.callbackUrl as string | undefined
const url = parseUrl(req.origin)
if (callbackUrlParam && !isValidHttpUrl(callbackUrlParam, url.base)) {
return new InvalidCallbackUrl(
`Invalid callback URL. Received: ${callbackUrlParam}`
)
}
const { callbackUrl: defaultCallbackUrl } = defaultCookies(
options.useSecureCookies ?? url.base.startsWith("https://")
)
const callbackUrlCookie =
req.cookies?.[options.cookies?.callbackUrl?.name ?? defaultCallbackUrl.name]
if (callbackUrlCookie && !isValidHttpUrl(callbackUrlCookie, url.base)) {
return new InvalidCallbackUrl(
`Invalid callback URL. Received: ${callbackUrlCookie}`
)
}
let hasCredentials, hasEmail
let hasTwitterOAuth2
for (const provider of options.providers) {
if (provider.type === "credentials") hasCredentials = true
else if (provider.type === "email") hasEmail = true
else if (provider.id === "twitter" && provider.version === "2.0")
hasTwitterOAuth2 = true
}
if (hasCredentials) {
const dbStrategy = options.session?.strategy === "database"
const onlyCredentials = !options.providers.some(
(p) => p.type !== "credentials"
)
if (dbStrategy && onlyCredentials) {
return new UnsupportedStrategy(
"Signin in with credentials only supported if JWT strategy is enabled"
)
}
const credentialsNoAuthorize = options.providers.some(
(p) => p.type === "credentials" && !p.authorize
)
if (credentialsNoAuthorize) {
return new MissingAuthorize(
"Must define an authorize() handler to use credentials authentication provider"
)
}
}
if (hasEmail) {
const { adapter } = options
if (!adapter) {
return new MissingAdapter("E-mail login requires an adapter.")
}
const missingMethods = [
"createVerificationToken",
"useVerificationToken",
"getUserByEmail",
].filter((method) => !adapter[method])
if (missingMethods.length) {
return new MissingAdapterMethods(
`Required adapter methods were missing: ${missingMethods.join(", ")}`
)
}
}
if (!warned) {
if (hasTwitterOAuth2) warnings.push("TWITTER_OAUTH_2_BETA")
warned = true
}
return warnings
}

View File

@@ -0,0 +1,229 @@
import { AccountNotLinkedError } from "../errors"
import { fromDate } from "./utils"
import type { InternalOptions } from "../types"
import type { AdapterSession, AdapterUser } from "../../adapters"
import type { JWT } from "../../jwt"
import type { Account, User } from "../.."
import type { SessionToken } from "./cookie"
import { OAuthConfig } from "src/providers"
/**
* This function handles the complex flow of signing users in, and either creating,
* linking (or not linking) accounts depending on if the user is currently logged
* in, if they have account already and the authentication mechanism they are using.
*
* It prevents insecure behaviour, such as linking OAuth accounts unless a user is
* signed in and authenticated with an existing valid account.
*
* All verification (e.g. OAuth flows or email address verificaiton flows) are
* done prior to this handler being called to avoid additonal complexity in this
* handler.
*/
export default async function callbackHandler(params: {
sessionToken?: SessionToken
profile: User | AdapterUser | { email: string }
account: Account | null
options: InternalOptions
}) {
const { sessionToken, profile: _profile, account, options } = params
// Input validation
if (!account?.providerAccountId || !account.type)
throw new Error("Missing or invalid provider account")
if (!["email", "oauth"].includes(account.type))
throw new Error("Provider not supported")
const {
adapter,
jwt,
events,
session: { strategy: sessionStrategy, generateSessionToken },
} = options
// If no adapter is configured then we don't have a database and cannot
// persist data; in this mode we just return a dummy session object.
if (!adapter) {
return { user: _profile as User, account }
}
const profile = _profile as AdapterUser
const {
createUser,
updateUser,
getUser,
getUserByAccount,
getUserByEmail,
linkAccount,
createSession,
getSessionAndUser,
deleteSession,
} = adapter
let session: AdapterSession | JWT | null = null
let user: AdapterUser | null = null
let isNewUser = false
const useJwtSession = sessionStrategy === "jwt"
if (sessionToken) {
if (useJwtSession) {
try {
session = await jwt.decode({ ...jwt, token: sessionToken })
if (session && "sub" in session && session.sub) {
user = await getUser(session.sub)
}
} catch {
// If session can't be verified, treat as no session
}
} else {
const userAndSession = await getSessionAndUser(sessionToken)
if (userAndSession) {
session = userAndSession.session
user = userAndSession.user
}
}
}
if (account.type === "email") {
// If signing in with an email, check if an account with the same email address exists already
const userByEmail = await getUserByEmail(profile.email)
if (userByEmail) {
// If they are not already signed in as the same user, this flow will
// sign them out of the current session and sign them in as the new user
if (user?.id !== userByEmail.id && !useJwtSession && sessionToken) {
// Delete existing session if they are currently signed in as another user.
// This will switch user accounts for the session in cases where the user was
// already logged in with a different account.
await deleteSession(sessionToken)
}
// Update emailVerified property on the user object
user = await updateUser({ id: userByEmail.id, emailVerified: new Date() })
await events.updateUser?.({ user })
} else {
const { id: _, ...newUser } = { ...profile, emailVerified: new Date() }
// Create user account if there isn't one for the email address already
user = await createUser(newUser)
await events.createUser?.({ user })
isNewUser = true
}
// Create new session
session = useJwtSession
? {}
: await createSession({
sessionToken: await generateSessionToken(),
userId: user.id,
expires: fromDate(options.session.maxAge),
})
return { session, user, isNewUser }
} else if (account.type === "oauth") {
// If signing in with OAuth account, check to see if the account exists already
const userByAccount = await getUserByAccount({
providerAccountId: account.providerAccountId,
provider: account.provider,
})
if (userByAccount) {
if (user) {
// If the user is already signed in with this account, we don't need to do anything
if (userByAccount.id === user.id) {
return { session, user, isNewUser }
}
// If the user is currently signed in, but the new account they are signing in
// with is already associated with another user, then we cannot link them
// and need to return an error.
throw new AccountNotLinkedError(
"The account is already associated with another user"
)
}
// If there is no active session, but the account being signed in with is already
// associated with a valid user then create session to sign the user in.
session = useJwtSession
? {}
: await createSession({
sessionToken: await generateSessionToken(),
userId: userByAccount.id,
expires: fromDate(options.session.maxAge),
})
return { session, user: userByAccount, isNewUser }
} else {
if (user) {
// If the user is already signed in and the OAuth account isn't already associated
// with another user account then we can go ahead and link the accounts safely.
await linkAccount({ ...account, userId: user.id })
await events.linkAccount?.({ user, account, profile })
// As they are already signed in, we don't need to do anything after linking them
return { session, user, isNewUser }
}
// If the user is not signed in and it looks like a new OAuth account then we
// check there also isn't an user account already associated with the same
// email address as the one in the OAuth profile.
//
// This step is often overlooked in OAuth implementations, but covers the following cases:
//
// 1. It makes it harder for someone to accidentally create two accounts.
// e.g. by signin in with email, then again with an oauth account connected to the same email.
// 2. It makes it harder to hijack a user account using a 3rd party OAuth account.
// e.g. by creating an oauth account then changing the email address associated with it.
//
// It's quite common for services to automatically link accounts in this case, but it's
// better practice to require the user to sign in *then* link accounts to be sure
// someone is not exploiting a problem with a third party OAuth service.
//
// OAuth providers should require email address verification to prevent this, but in
// practice that is not always the case; this helps protect against that.
const userByEmail = profile.email
? await getUserByEmail(profile.email)
: null
if (userByEmail) {
const provider = options.provider as OAuthConfig<any>
if (provider?.allowDangerousEmailAccountLinking) {
// If you trust the oauth provider to correctly verify email addresses, you can opt-in to
// account linking even when the user is not signed-in.
user = userByEmail
} else {
// We end up here when we don't have an account with the same [provider].id *BUT*
// we do already have an account with the same email address as the one in the
// OAuth profile the user has just tried to sign in with.
//
// We don't want to have two accounts with the same email address, and we don't
// want to link them in case it's not safe to do so, so instead we prompt the user
// to sign in via email to verify their identity and then link the accounts.
throw new AccountNotLinkedError(
"Another account already exists with the same e-mail address"
)
}
} else {
// If the current user is not logged in and the profile isn't linked to any user
// accounts (by email or provider account id)...
//
// If no account matching the same [provider].id or .email exists, we can
// create a new account for the user, link it to the OAuth acccount and
// create a new session for them so they are signed in with it.
const { id: _, ...newUser } = { ...profile, emailVerified: null }
user = await createUser(newUser)
}
await events.createUser?.({ user })
await linkAccount({ ...account, userId: user.id })
await events.linkAccount?.({ user, account, profile })
session = useJwtSession
? {}
: await createSession({
sessionToken: await generateSessionToken(),
userId: user.id,
expires: fromDate(options.session.maxAge),
})
return { session, user, isNewUser: true }
}
}
throw new Error("Unsupported account type")
}

View File

@@ -0,0 +1,42 @@
import type { InternalOptions } from "../types"
interface CreateCallbackUrlParams {
options: InternalOptions
/** Try reading value from request body (POST) then from query param (GET) */
paramValue?: string
cookieValue?: string
}
/**
* Get callback URL based on query param / cookie + validation,
* and add it to `req.options.callbackUrl`.
*/
export async function createCallbackUrl({
options,
paramValue,
cookieValue,
}: CreateCallbackUrlParams) {
const { url, callbacks } = options
let callbackUrl = url.origin
if (paramValue) {
// If callbackUrl form field or query parameter is passed try to use it if allowed
callbackUrl = await callbacks.redirect({
url: paramValue,
baseUrl: url.origin,
})
} else if (cookieValue) {
// If no callbackUrl specified, try using the value from the cookie if allowed
callbackUrl = await callbacks.redirect({
url: cookieValue,
baseUrl: url.origin,
})
}
return {
callbackUrl,
// Save callback URL in a cookie so that it can be used for subsequent requests in signin/signout/callback flow
callbackUrlCookie: callbackUrl !== cookieValue ? callbackUrl : undefined,
}
}

View File

@@ -0,0 +1,237 @@
import type { CookiesOptions } from "../.."
import type { CookieOption, LoggerInstance, SessionStrategy } from "../types"
import type { NextRequest } from "next/server"
import type { NextApiRequest } from "next"
// Uncomment to recalculate the estimated size
// of an empty session cookie
// import { serialize } from "cookie"
// console.log(
// "Cookie estimated to be ",
// serialize(`__Secure.next-auth.session-token.0`, "", {
// expires: new Date(),
// httpOnly: true,
// maxAge: Number.MAX_SAFE_INTEGER,
// path: "/",
// sameSite: "strict",
// secure: true,
// domain: "example.com",
// }).length,
// " bytes"
// )
const ALLOWED_COOKIE_SIZE = 4096
// Based on commented out section above
const ESTIMATED_EMPTY_COOKIE_SIZE = 163
const CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE
// REVIEW: Is there any way to defer two types of strings?
/** Stringified form of `JWT`. Extract the content with `jwt.decode` */
export type JWTString = string
export type SetCookieOptions = Partial<CookieOption["options"]> & {
expires?: Date | string
encode?: (val: unknown) => string
}
/**
* If `options.session.strategy` is set to `jwt`, this is a stringified `JWT`.
* In case of `strategy: "database"`, this is the `sessionToken` of the session in the database.
*/
export type SessionToken<T extends SessionStrategy = "jwt"> = T extends "jwt"
? JWTString
: string
/**
* Use secure cookies if the site uses HTTPS
* This being conditional allows cookies to work non-HTTPS development URLs
* Honour secure cookie option, which sets 'secure' and also adds '__Secure-'
* prefix, but enable them by default if the site URL is HTTPS; but not for
* non-HTTPS URLs like http://localhost which are used in development).
* For more on prefixes see https://googlechrome.github.io/samples/cookie-prefixes/
*
* @TODO Review cookie settings (names, options)
*/
export function defaultCookies(useSecureCookies: boolean): CookiesOptions {
const cookiePrefix = useSecureCookies ? "__Secure-" : ""
return {
// default cookie options
sessionToken: {
name: `${cookiePrefix}next-auth.session-token`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: useSecureCookies,
},
},
callbackUrl: {
name: `${cookiePrefix}next-auth.callback-url`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: useSecureCookies,
},
},
csrfToken: {
// Default to __Host- for CSRF token for additional protection if using useSecureCookies
// NB: The `__Host-` prefix is stricter than the `__Secure-` prefix.
name: `${useSecureCookies ? "__Host-" : ""}next-auth.csrf-token`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: useSecureCookies,
},
},
pkceCodeVerifier: {
name: `${cookiePrefix}next-auth.pkce.code_verifier`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: useSecureCookies,
maxAge: 60 * 15, // 15 minutes in seconds
},
},
state: {
name: `${cookiePrefix}next-auth.state`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: useSecureCookies,
maxAge: 60 * 15, // 15 minutes in seconds
},
},
nonce: {
name: `${cookiePrefix}next-auth.nonce`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: useSecureCookies,
},
}
}
}
export interface Cookie extends CookieOption {
value: string
}
type Chunks = Record<string, string>
export class SessionStore {
#chunks: Chunks = {}
#option: CookieOption
#logger: LoggerInstance | Console
constructor(
option: CookieOption,
req: Partial<{
cookies: NextRequest["cookies"] | NextApiRequest["cookies"]
headers: NextRequest["headers"] | NextApiRequest["headers"]
}>,
logger: LoggerInstance | Console
) {
this.#logger = logger
this.#option = option
const { cookies } = req
const { name: cookieName } = option
if (typeof cookies?.getAll === "function") {
// Next.js ^v13.0.1 (Edge Env)
for (const { name, value } of cookies.getAll()) {
if (name.startsWith(cookieName)) {
this.#chunks[name] = value
}
}
} else if (cookies instanceof Map) {
for (const name of cookies.keys()) {
if (name.startsWith(cookieName)) this.#chunks[name] = cookies.get(name)
}
} else {
for (const name in cookies) {
if (name.startsWith(cookieName)) this.#chunks[name] = cookies[name]
}
}
}
get value() {
return Object.values(this.#chunks)?.join("")
}
/** Given a cookie, return a list of cookies, chunked to fit the allowed cookie size. */
#chunk(cookie: Cookie): Cookie[] {
const chunkCount = Math.ceil(cookie.value.length / CHUNK_SIZE)
if (chunkCount === 1) {
this.#chunks[cookie.name] = cookie.value
return [cookie]
}
const cookies: Cookie[] = []
for (let i = 0; i < chunkCount; i++) {
const name = `${cookie.name}.${i}`
const value = cookie.value.substr(i * CHUNK_SIZE, CHUNK_SIZE)
cookies.push({ ...cookie, name, value })
this.#chunks[name] = value
}
this.#logger.debug("CHUNKING_SESSION_COOKIE", {
message: `Session cookie exceeds allowed ${ALLOWED_COOKIE_SIZE} bytes.`,
emptyCookieSize: ESTIMATED_EMPTY_COOKIE_SIZE,
valueSize: cookie.value.length,
chunks: cookies.map((c) => c.value.length + ESTIMATED_EMPTY_COOKIE_SIZE),
})
return cookies
}
/** Returns cleaned cookie chunks. */
#clean(): Record<string, Cookie> {
const cleanedChunks: Record<string, Cookie> = {}
for (const name in this.#chunks) {
delete this.#chunks?.[name]
cleanedChunks[name] = {
name,
value: "",
options: { ...this.#option.options, maxAge: 0 },
}
}
return cleanedChunks
}
/**
* Given a cookie value, return new cookies, chunked, to fit the allowed cookie size.
* If the cookie has changed from chunked to unchunked or vice versa,
* it deletes the old cookies as well.
*/
chunk(value: string, options: Partial<Cookie["options"]>): Cookie[] {
// Assume all cookies should be cleaned by default
const cookies: Record<string, Cookie> = this.#clean()
// Calculate new chunks
const chunked = this.#chunk({
name: this.#option.name,
value,
options: { ...this.#option.options, ...options },
})
// Update stored chunks / cookies
for (const chunk of chunked) {
cookies[chunk.name] = chunk
}
return Object.values(cookies)
}
/** Returns a list of cookies that should be cleaned. */
clean(): Cookie[] {
return Object.values(this.#clean())
}
}

View File

@@ -0,0 +1,55 @@
import { createHash, randomBytes } from "crypto"
import type { InternalOptions } from "../types"
interface CreateCSRFTokenParams {
options: InternalOptions
cookieValue?: string
isPost: boolean
bodyValue?: string
}
/**
* Ensure CSRF Token cookie is set for any subsequent requests.
* Used as part of the strategy for mitigation for CSRF tokens.
*
* Creates a cookie like 'next-auth.csrf-token' with the value 'token|hash',
* where 'token' is the CSRF token and 'hash' is a hash made of the token and
* the secret, and the two values are joined by a pipe '|'. By storing the
* value and the hash of the value (with the secret used as a salt) we can
* verify the cookie was set by the server and not by a malicous attacker.
*
* For more details, see the following OWASP links:
* https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie
* https://owasp.org/www-chapter-london/assets/slides/David_Johansson-Double_Defeat_of_Double-Submit_Cookie.pdf
*/
export function createCSRFToken({
options,
cookieValue,
isPost,
bodyValue,
}: CreateCSRFTokenParams) {
if (cookieValue) {
const [csrfToken, csrfTokenHash] = cookieValue.split("|")
const expectedCsrfTokenHash = createHash("sha256")
.update(`${csrfToken}${options.secret}`)
.digest("hex")
if (csrfTokenHash === expectedCsrfTokenHash) {
// If hash matches then we trust the CSRF token value
// If this is a POST request and the CSRF Token in the POST request matches
// the cookie we have already verified is the one we have set, then the token is verified!
const csrfTokenVerified = isPost && csrfToken === bodyValue
return { csrfTokenVerified, csrfToken }
}
}
// New CSRF token
const csrfToken = randomBytes(32).toString("hex")
const csrfTokenHash = createHash("sha256")
.update(`${csrfToken}${options.secret}`)
.digest("hex")
const cookie = `${csrfToken}|${csrfTokenHash}`
return { cookie, csrfToken }
}

View File

@@ -0,0 +1,18 @@
import { CallbacksOptions } from "../.."
export const defaultCallbacks: CallbacksOptions = {
signIn() {
return true
},
redirect({ url, baseUrl }) {
if (url.startsWith("/")) return `${baseUrl}${url}`
else if (new URL(url).origin === baseUrl) return url
return baseUrl
},
session({ session }) {
return session
},
jwt({ token }) {
return token
},
}

View File

@@ -0,0 +1,20 @@
import type { AdapterUser } from "../../../adapters"
import type { InternalOptions } from "../../types"
/**
* Query the database for a user by email address.
* If is an existing user return a user object (otherwise use placeholder).
*/
export default async function getAdapterUserFromEmail({
email,
adapter,
}: {
email: string
adapter: InternalOptions<"email">["adapter"]
}): Promise<AdapterUser> {
const { getUserByEmail } = adapter
const adapterUser = email ? await getUserByEmail(email) : null
if (adapterUser) return adapterUser
return { id: email, email, emailVerified: null }
}

View File

@@ -0,0 +1,50 @@
import { randomBytes } from "crypto"
import { hashToken } from "../utils"
import type { InternalOptions } from "../../types"
/**
* Starts an e-mail login flow, by generating a token,
* and sending it to the user's e-mail (with the help of a DB adapter)
*/
export default async function email(
identifier: string,
options: InternalOptions<"email">
): Promise<string> {
const { url, adapter, provider, callbackUrl, theme } = options
// Generate token
const token =
(await provider.generateVerificationToken?.()) ??
randomBytes(32).toString("hex")
const ONE_DAY_IN_SECONDS = 86400
const expires = new Date(
Date.now() + (provider.maxAge ?? ONE_DAY_IN_SECONDS) * 1000
)
// Generate a link with email, unhashed token and callback url
const params = new URLSearchParams({ callbackUrl, token, email: identifier })
const _url = `${url}/callback/${provider.id}?${params}`
await Promise.all([
// Send to user
provider.sendVerificationRequest({
identifier,
token,
expires,
url: _url,
provider,
theme,
}),
// Save in database
adapter.createVerificationToken({
identifier,
token: hashToken(token, options),
expires,
}),
])
return `${url}/verify-request?${new URLSearchParams({
provider: provider.id,
type: provider.type,
})}`
}

View File

@@ -0,0 +1,63 @@
import { openidClient } from "./client"
import { oAuth1Client, oAuth1TokenStore } from "./client-legacy"
import * as checks from "./checks"
import type { AuthorizationParameters } from "openid-client"
import type { InternalOptions } from "../../types"
import type { RequestInternal } from "../.."
import type { Cookie } from "../cookie"
/**
*
* Generates an authorization/request token URL.
*
* [OAuth 2](https://www.oauth.com/oauth2-servers/authorization/the-authorization-request/) | [OAuth 1](https://oauth.net/core/1.0a/#auth_step2)
*/
export default async function getAuthorizationUrl({
options,
query,
}: {
options: InternalOptions<"oauth">
query: RequestInternal["query"]
}) {
const { logger, provider } = options
let params: any = {}
if (typeof provider.authorization === "string") {
const parsedUrl = new URL(provider.authorization)
const parsedParams = Object.fromEntries(parsedUrl.searchParams)
params = { ...params, ...parsedParams }
} else {
params = { ...params, ...provider.authorization?.params }
}
params = { ...params, ...query }
// Handle OAuth v1.x
if (provider.version?.startsWith("1.")) {
const client = oAuth1Client(options)
const tokens = (await client.getOAuthRequestToken(params)) as any
const url = `${provider.authorization?.url}?${new URLSearchParams({
oauth_token: tokens.oauth_token,
oauth_token_secret: tokens.oauth_token_secret,
...tokens.params,
})}`
oAuth1TokenStore.set(tokens.oauth_token, tokens.oauth_token_secret)
logger.debug("GET_AUTHORIZATION_URL", { url, provider })
return { redirect: url }
}
const client = await openidClient(options)
const authorizationParams: AuthorizationParameters = params
const cookies: Cookie[] = []
await checks.state.create(options, cookies, authorizationParams)
await checks.pkce.create(options, cookies, authorizationParams)
await checks.nonce.create(options, cookies, authorizationParams)
const url = client.authorizationUrl(authorizationParams)
logger.debug("GET_AUTHORIZATION_URL", { url, cookies, provider })
return { redirect: url, cookies }
}

View File

@@ -0,0 +1,183 @@
import { TokenSet } from "openid-client"
import { openidClient } from "./client"
import { oAuth1Client, oAuth1TokenStore } from "./client-legacy"
import * as _checks from "./checks"
import { OAuthCallbackError } from "../../errors"
import type { CallbackParamsType } from "openid-client"
import type { LoggerInstance, Profile } from "../../.."
import type { OAuthChecks, OAuthConfig } from "../../../providers"
import type { InternalOptions } from "../../types"
import type { RequestInternal } from "../.."
import type { Cookie } from "../cookie"
export default async function oAuthCallback(params: {
options: InternalOptions<"oauth">
query: RequestInternal["query"]
body: RequestInternal["body"]
method: Required<RequestInternal>["method"]
cookies: RequestInternal["cookies"]
}) {
const { options, query, body, method, cookies } = params
const { logger, provider } = options
const errorMessage = body?.error ?? query?.error
if (errorMessage) {
const error = new Error(errorMessage)
logger.error("OAUTH_CALLBACK_HANDLER_ERROR", {
error,
error_description: query?.error_description,
providerId: provider.id,
})
logger.debug("OAUTH_CALLBACK_HANDLER_ERROR", { body })
throw error
}
if (provider.version?.startsWith("1.")) {
try {
const client = await oAuth1Client(options)
// Handle OAuth v1.x
const { oauth_token, oauth_verifier } = query ?? {}
const tokens = (await (client as any).getOAuthAccessToken(
oauth_token,
oAuth1TokenStore.get(oauth_token),
oauth_verifier
)) as TokenSet
let profile: Profile = await (client as any).get(
provider.profileUrl,
tokens.oauth_token,
tokens.oauth_token_secret
)
if (typeof profile === "string") {
profile = JSON.parse(profile)
}
const newProfile = await getProfile({ profile, tokens, provider, logger })
return { ...newProfile, cookies: [] }
} catch (error) {
logger.error("OAUTH_V1_GET_ACCESS_TOKEN_ERROR", error as Error)
throw error
}
}
if (query?.oauth_token) oAuth1TokenStore.delete(query.oauth_token)
try {
const client = await openidClient(options)
let tokens: TokenSet
const checks: OAuthChecks = {}
const resCookies: Cookie[] = []
await _checks.state.use(cookies, resCookies, options, checks)
await _checks.pkce.use(cookies, resCookies, options, checks)
await _checks.nonce.use(cookies, resCookies, options, checks)
const params: CallbackParamsType = {
...client.callbackParams({
url: `http://n?${new URLSearchParams(query)}`,
// TODO: Ask to allow object to be passed upstream:
// https://github.com/panva/node-openid-client/blob/3ae206dfc78c02134aa87a07f693052c637cab84/types/index.d.ts#L439
// @ts-expect-error
body,
method,
}),
...provider.token?.params,
}
if (provider.token?.request) {
const response = await provider.token.request({
provider,
params,
checks,
client,
})
tokens = new TokenSet(response.tokens)
} else if (provider.idToken) {
tokens = await client.callback(provider.callbackUrl, params, checks)
} else {
tokens = await client.oauthCallback(provider.callbackUrl, params, checks)
}
// REVIEW: How can scope be returned as an array?
if (Array.isArray(tokens.scope)) {
tokens.scope = tokens.scope.join(" ")
}
let profile: Profile
if (provider.userinfo?.request) {
profile = await provider.userinfo.request({
provider,
tokens,
client,
})
} else if (provider.idToken) {
profile = tokens.claims()
} else {
profile = await client.userinfo(tokens, {
params: provider.userinfo?.params,
})
}
const profileResult = await getProfile({
profile,
provider,
tokens,
logger,
})
return { ...profileResult, cookies: resCookies }
} catch (error) {
throw new OAuthCallbackError(error as Error)
}
}
export interface GetProfileParams {
profile: Profile
tokens: TokenSet
provider: OAuthConfig<any>
logger: LoggerInstance
}
/** Returns profile, raw profile and auth provider details */
async function getProfile({
profile: OAuthProfile,
tokens,
provider,
logger,
}: GetProfileParams) {
try {
logger.debug("PROFILE_DATA", { OAuthProfile })
const profile = await provider.profile(OAuthProfile, tokens)
profile.email = profile.email?.toLowerCase()
if (!profile.id)
throw new TypeError(
`Profile id is missing in ${provider.name} OAuth profile response`
)
// Return profile, raw profile and auth provider details
return {
profile,
account: {
provider: provider.id,
type: provider.type,
providerAccountId: profile.id.toString(),
...tokens,
},
OAuthProfile,
}
} catch (error) {
// If we didn't get a response either there was a problem with the provider
// response *or* the user cancelled the action with the provider.
//
// Unfortuately, we can't tell which - at least not in a way that works for
// all providers, so we return an empty object; the user should then be
// redirected back to the sign up page. We log the error to help developers
// who might be trying to debug this when configuring a new provider.
logger.error("OAUTH_PARSE_PROFILE_ERROR", {
error: error as Error,
OAuthProfile,
})
}
}

View File

@@ -0,0 +1,181 @@
import {
AuthorizationParameters,
generators,
OpenIDCallbackChecks,
} from "openid-client"
import * as jwt from "../../../jwt"
import type { RequestInternal } from "../.."
import type { OAuthChecks } from "../../../providers"
import type { CookiesOptions, InternalOptions } from "../../types"
import type { Cookie } from "../cookie"
/** Returns a signed cookie. */
export async function signCookie(
type: keyof CookiesOptions,
value: string,
maxAge: number,
options: InternalOptions<"oauth">
): Promise<Cookie> {
const { cookies, logger } = options
logger.debug(`CREATE_${type.toUpperCase()}`, { value, maxAge })
const expires = new Date()
expires.setTime(expires.getTime() + maxAge * 1000)
return {
name: cookies[type].name,
value: await jwt.encode({ ...options.jwt, maxAge, token: { value } }),
options: { ...cookies[type].options, expires },
}
}
const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
export const PKCE_CODE_CHALLENGE_METHOD = "S256"
export const pkce = {
async create(
options: InternalOptions<"oauth">,
cookies: Cookie[],
resParams: AuthorizationParameters
) {
if (!options.provider?.checks?.includes("pkce")) return
const code_verifier = generators.codeVerifier()
const value = generators.codeChallenge(code_verifier)
resParams.code_challenge = value
resParams.code_challenge_method = PKCE_CODE_CHALLENGE_METHOD
const maxAge =
options.cookies.pkceCodeVerifier.options.maxAge ?? PKCE_MAX_AGE
cookies.push(
await signCookie("pkceCodeVerifier", code_verifier, maxAge, options)
)
},
/**
* Returns code_verifier if the provider is configured to use PKCE,
* and clears the container cookie afterwards.
* An error is thrown if the code_verifier is missing or invalid.
* @see https://www.rfc-editor.org/rfc/rfc7636
* @see https://danielfett.de/2020/05/16/pkce-vs-nonce-equivalent-or-not/#pkce
*/
async use(
cookies: RequestInternal["cookies"],
resCookies: Cookie[],
options: InternalOptions<"oauth">,
checks: OAuthChecks
): Promise<string | undefined> {
if (!options.provider?.checks?.includes("pkce")) return
const codeVerifier = cookies?.[options.cookies.pkceCodeVerifier.name]
if (!codeVerifier)
throw new TypeError("PKCE code_verifier cookie was missing.")
const value = (await jwt.decode({
...options.jwt,
token: codeVerifier,
})) as any
if (!value?.value)
throw new TypeError("PKCE code_verifier value could not be parsed.")
resCookies.push({
name: options.cookies.pkceCodeVerifier.name,
value: "",
options: { ...options.cookies.pkceCodeVerifier.options, maxAge: 0 },
})
checks.code_verifier = value.value
},
}
const STATE_MAX_AGE = 60 * 15 // 15 minutes in seconds
export const state = {
async create(
options: InternalOptions<"oauth">,
cookies: Cookie[],
resParams: AuthorizationParameters
) {
if (!options.provider.checks?.includes("state")) return
const value = generators.state()
resParams.state = value
const maxAge = options.cookies.state.options.maxAge ?? STATE_MAX_AGE
cookies.push(await signCookie("state", value, maxAge, options))
},
/**
* Returns state if the provider is configured to use state,
* and clears the container cookie afterwards.
* An error is thrown if the state is missing or invalid.
* @see https://www.rfc-editor.org/rfc/rfc6749#section-10.12
* @see https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1
*/
async use(
cookies: RequestInternal["cookies"],
resCookies: Cookie[],
options: InternalOptions<"oauth">,
checks: OAuthChecks
) {
if (!options.provider.checks?.includes("state")) return
const state = cookies?.[options.cookies.state.name]
if (!state) throw new TypeError("State cookie was missing.")
const value = (await jwt.decode({ ...options.jwt, token: state })) as any
if (!value?.value) throw new TypeError("State value could not be parsed.")
resCookies.push({
name: options.cookies.state.name,
value: "",
options: { ...options.cookies.state.options, maxAge: 0 },
})
checks.state = value.value
},
}
const NONCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
export const nonce = {
async create(
options: InternalOptions<"oauth">,
cookies: Cookie[],
resParams: AuthorizationParameters
) {
if (!options.provider.checks?.includes("nonce")) return
const value = generators.nonce()
resParams.nonce = value
const maxAge = options.cookies.nonce.options.maxAge ?? NONCE_MAX_AGE
cookies.push(await signCookie("nonce", value, maxAge, options))
},
/**
* Returns nonce if the provider is configured to use nonce,
* and clears the container cookie afterwards.
* An error is thrown if the nonce is missing or invalid.
* @see https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes
* @see https://danielfett.de/2020/05/16/pkce-vs-nonce-equivalent-or-not/#nonce
*/
async use(
cookies: RequestInternal["cookies"],
resCookies: Cookie[],
options: InternalOptions<"oauth">,
checks: OpenIDCallbackChecks
): Promise<string | undefined> {
if (!options.provider?.checks?.includes("nonce")) return
const nonce = cookies?.[options.cookies.nonce.name]
if (!nonce) throw new TypeError("Nonce cookie was missing.")
const value = (await jwt.decode({ ...options.jwt, token: nonce })) as any
if (!value?.value) throw new TypeError("Nonce value could not be parsed.")
resCookies.push({
name: options.cookies.nonce.name,
value: "",
options: { ...options.cookies.nonce.options, maxAge: 0 },
})
checks.nonce = value.value
},
}

View File

@@ -0,0 +1,73 @@
// This is kept around for being backwards compatible with OAuth 1.0 providers.
// We have the intentions to provide only minor fixes for this in the future.
import { OAuth } from "oauth"
import type { InternalOptions } from "../../types"
/**
* Client supporting OAuth 1.x
*/
export function oAuth1Client(options: InternalOptions<"oauth">) {
const provider = options.provider
const oauth1Client = new OAuth(
provider.requestTokenUrl as string,
provider.accessTokenUrl as string,
provider.clientId as string,
provider.clientSecret as string,
provider.version ?? "1.0",
provider.callbackUrl,
provider.encoding ?? "HMAC-SHA1"
)
// Promisify get() for OAuth1
const originalGet = oauth1Client.get.bind(oauth1Client)
// @ts-expect-error
oauth1Client.get = async (...args) => {
return await new Promise((resolve, reject) => {
originalGet(...args, (error, result) => {
if (error) {
return reject(error)
}
resolve(result)
})
})
}
// Promisify getOAuth1AccessToken() for OAuth1
const originalGetOAuth1AccessToken =
oauth1Client.getOAuthAccessToken.bind(oauth1Client)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
oauth1Client.getOAuthAccessToken = async (...args: any[]) => {
return await new Promise((resolve, reject) => {
originalGetOAuth1AccessToken(
...args,
(error: any, oauth_token: any, oauth_token_secret: any) => {
if (error) {
return reject(error)
}
resolve({ oauth_token, oauth_token_secret } as any)
}
)
})
}
const originalGetOAuthRequestToken =
oauth1Client.getOAuthRequestToken.bind(oauth1Client)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
oauth1Client.getOAuthRequestToken = async (params = {}) => {
return await new Promise((resolve, reject) => {
originalGetOAuthRequestToken(
params,
(error, oauth_token, oauth_token_secret, params) => {
if (error) {
return reject(error)
}
resolve({ oauth_token, oauth_token_secret, params } as any)
}
)
})
}
return oauth1Client
}
export const oAuth1TokenStore = new Map()

View File

@@ -0,0 +1,48 @@
import { Issuer, custom } from "openid-client"
import type { Client } from "openid-client"
import type { InternalOptions } from "../../types"
/**
* NOTE: We can add auto discovery of the provider's endpoint
* that requires only one endpoint to be specified by the user.
* Check out `Issuer.discover`
*
* Client supporting OAuth 2.x and OIDC
*/
export async function openidClient(
options: InternalOptions<"oauth">
): Promise<Client> {
const provider = options.provider
if (provider.httpOptions) custom.setHttpOptionsDefaults(provider.httpOptions)
let issuer: Issuer
if (provider.wellKnown) {
issuer = await Issuer.discover(provider.wellKnown)
} else {
issuer = new Issuer({
issuer: provider.issuer as string,
authorization_endpoint: provider.authorization?.url,
token_endpoint: provider.token?.url,
userinfo_endpoint: provider.userinfo?.url,
jwks_uri: provider.jwks_endpoint,
})
}
const client = new issuer.Client(
{
client_id: provider.clientId as string,
client_secret: provider.clientSecret as string,
redirect_uris: [provider.callbackUrl],
...provider.client,
},
provider.jwks
)
// allow a 10 second skew
// See https://github.com/nextauthjs/next-auth/issues/3032
// and https://github.com/nextauthjs/next-auth/issues/3067
client[custom.clock_tolerance] = 10
return client
}

View File

@@ -0,0 +1,93 @@
import { merge } from "../../utils/merge"
import type { InternalProvider, OAuthConfigInternal } from "../types"
import type { OAuthConfig, Provider } from "../../providers"
import type { InternalUrl } from "../../utils/parse-url"
/**
* Adds `signinUrl` and `callbackUrl` to each provider
* and deep merge user-defined options.
*/
export default function parseProviders(params: {
providers: Provider[]
url: InternalUrl
providerId?: string
}): {
providers: InternalProvider[]
provider?: InternalProvider
} {
const { url, providerId } = params
const providers = params.providers.map<InternalProvider>(
({ options: userOptions, ...rest }) => {
if (rest.type === "oauth") {
const normalizedOptions = normalizeOAuthOptions(rest)
const normalizedUserOptions = normalizeOAuthOptions(userOptions, true)
const id = normalizedUserOptions?.id ?? rest.id
return merge(normalizedOptions, {
...normalizedUserOptions,
signinUrl: `${url}/signin/${id}`,
callbackUrl: `${url}/callback/${id}`,
})
}
const id = (userOptions?.id as string) ?? rest.id
return merge(rest, {
...userOptions,
signinUrl: `${url}/signin/${id}`,
callbackUrl: `${url}/callback/${id}`,
})
}
)
return {
providers,
provider: providers.find(({ id }) => id === providerId),
}
}
/**
* Transform OAuth options `authorization`, `token` and `profile` strings to `{ url: string; params: Record<string, string> }`
*/
function normalizeOAuthOptions(
oauthOptions?: Partial<OAuthConfig<any>> | Record<string, unknown>,
isUserOptions = false
) {
if (!oauthOptions) return
const normalized = Object.entries(oauthOptions).reduce<
OAuthConfigInternal<Record<string, unknown>>
>(
(acc, [key, value]) => {
if (
["authorization", "token", "userinfo"].includes(key) &&
typeof value === "string"
) {
const url = new URL(value)
acc[key] = {
url: `${url.origin}${url.pathname}`,
params: Object.fromEntries(url.searchParams ?? []),
}
} else {
acc[key] = value
}
return acc
},
// eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter
{} as any
)
if (!isUserOptions && !normalized.version?.startsWith("1.")) {
// If provider has as an "openid-configuration" well-known endpoint
// or an "openid" scope request, it will also likely be able to receive an `id_token`
// Only do this if this function is not called with user options to avoid overriding in later stage.
normalized.idToken = Boolean(
normalized.idToken ??
normalized.wellKnown?.includes("openid-configuration") ??
normalized.authorization?.params?.scope?.includes("openid")
)
if (!normalized.checks) normalized.checks = ["state"]
}
return normalized
}

View File

@@ -0,0 +1,44 @@
import { createHash } from "crypto"
import type { AuthOptions } from "../.."
import type { InternalOptions } from "../types"
import type { InternalUrl } from "../../utils/parse-url"
/**
* Takes a number in seconds and returns the date in the future.
* Optionally takes a second date parameter. In that case
* the date in the future will be calculated from that date instead of now.
*/
export function fromDate(time: number, date = Date.now()) {
return new Date(date + time * 1000)
}
export function hashToken(token: string, options: InternalOptions<"email">) {
const { provider, secret } = options
return (
createHash("sha256")
// Prefer provider specific secret, but use default secret if none specified
.update(`${token}${provider.secret ?? secret}`)
.digest("hex")
)
}
/**
* Secret used salt cookies and tokens (e.g. for CSRF protection).
* If no secret option is specified then it creates one on the fly
* based on options passed here. If options contains unique data, such as
* OAuth provider secrets and database credentials it should be sufficent. If no secret provided in production, we throw an error. */
export function createSecret(params: {
authOptions: AuthOptions
url: InternalUrl
}) {
const { authOptions, url } = params
return (
authOptions.secret ??
// TODO: Remove falling back to default secret, and error in dev if one isn't provided
createHash("sha256")
.update(JSON.stringify({ ...url, ...authOptions }))
.digest("hex")
)
}

View File

@@ -0,0 +1,114 @@
import { Theme } from "../.."
import { InternalUrl } from "../../utils/parse-url"
/**
* The following errors are passed as error query parameters to the default or overridden error page.
*
* [Documentation](https://next-auth.js.org/configuration/pages#error-page) */
export type ErrorType =
| "default"
| "configuration"
| "accessdenied"
| "verification"
export interface ErrorProps {
url?: InternalUrl
theme?: Theme
error?: ErrorType
}
interface ErrorView {
status: number
heading: string
message: JSX.Element
signin?: JSX.Element
}
/** Renders an error page. */
export default function ErrorPage(props: ErrorProps) {
const { url, error = "default", theme } = props
const signinPageUrl = `${url}/signin`
const errors: Record<ErrorType, ErrorView> = {
default: {
status: 200,
heading: "Error",
message: (
<p>
<a className="site" href={url?.origin}>
{url?.host}
</a>
</p>
),
},
configuration: {
status: 500,
heading: "Server error",
message: (
<div>
<p>There is a problem with the server configuration.</p>
<p>Check the server logs for more information.</p>
</div>
),
},
accessdenied: {
status: 403,
heading: "Access Denied",
message: (
<div>
<p>You do not have permission to sign in.</p>
<p>
<a className="button" href={signinPageUrl}>
Sign in
</a>
</p>
</div>
),
},
verification: {
status: 403,
heading: "Unable to sign in",
message: (
<div>
<p>The sign in link is no longer valid.</p>
<p>It may have been used already or it may have expired.</p>
</div>
),
signin: (
<p>
<a className="button" href={signinPageUrl}>
Sign in
</a>
</p>
),
},
}
const { status, heading, message, signin } =
errors[error.toLowerCase()] ?? errors.default
return {
status,
html: (
<div className="error">
{theme?.brandColor && (
<style
dangerouslySetInnerHTML={{
__html: `
:root {
--brand-color: ${theme?.brandColor}
}
`,
}}
/>
)}
<div className="card">
{theme?.logo && <img src={theme.logo} alt="Logo" className="logo" />}
<h1>{heading}</h1>
<div className="message">{message}</div>
{signin}
</div>
</div>
),
}
}

View File

@@ -0,0 +1,79 @@
import renderToString from "preact-render-to-string"
import SigninPage from "./signin"
import SignoutPage from "./signout"
import VerifyRequestPage from "./verify-request"
import ErrorPage from "./error"
import css from "../../css"
import type { InternalOptions } from "../types"
import type { RequestInternal, ResponseInternal } from ".."
import type { Cookie } from "../lib/cookie"
import type { ErrorType } from "./error"
type RenderPageParams = {
query?: RequestInternal["query"]
cookies?: Cookie[]
} & Partial<
Pick<
InternalOptions,
"url" | "callbackUrl" | "csrfToken" | "providers" | "theme"
>
>
/**
* Unless the user defines their [own pages](https://next-auth.js.org/configuration/pages),
* we render a set of default ones, using Preact SSR.
*/
export default function renderPage(params: RenderPageParams) {
const { url, theme, query, cookies } = params
function send({ html, title, status }: any): ResponseInternal {
return {
cookies,
status,
headers: [{ key: "Content-Type", value: "text/html" }],
body: `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>${css()}</style><title>${title}</title></head><body class="__next-auth-theme-${
theme?.colorScheme ?? "auto"
}"><div class="page">${renderToString(html)}</div></body></html>`,
}
}
return {
signin(props?: any) {
return send({
html: SigninPage({
csrfToken: params.csrfToken,
providers: params.providers,
callbackUrl: params.callbackUrl,
theme,
...query,
...props,
}),
title: "Sign In",
})
},
signout(props?: any) {
return send({
html: SignoutPage({
csrfToken: params.csrfToken,
url,
theme,
...props,
}),
title: "Sign Out",
})
},
verifyRequest(props?: any) {
return send({
html: VerifyRequestPage({ url, theme, ...props }),
title: "Verify Request",
})
},
error(props?: { error?: ErrorType }) {
return send({
...ErrorPage({ url, theme, ...props }),
title: "Error",
})
},
}
}

View File

@@ -0,0 +1,222 @@
import type { InternalProvider, Theme } from "../types"
import type React from "react"
/**
* The following errors are passed as error query parameters to the default or overridden sign-in page.
*
* [Documentation](https://next-auth.js.org/configuration/pages#sign-in-page) */
export type SignInErrorTypes =
| "Signin"
| "OAuthSignin"
| "OAuthCallback"
| "OAuthCreateAccount"
| "EmailCreateAccount"
| "Callback"
| "OAuthAccountNotLinked"
| "EmailSignin"
| "CredentialsSignin"
| "SessionRequired"
| "default"
export interface SignInServerPageParams {
csrfToken: string
providers: InternalProvider[]
callbackUrl: string
email: string
error: SignInErrorTypes
theme: Theme
}
export default function SigninPage(props: SignInServerPageParams) {
const {
csrfToken,
providers,
callbackUrl,
theme,
email,
error: errorType,
} = props
// We only want to render providers
const providersToRender = providers.filter((provider) => {
if (provider.type === "oauth" || provider.type === "email") {
// Always render oauth and email type providers
return true
} else if (provider.type === "credentials" && provider.credentials) {
// Only render credentials type provider if credentials are defined
return true
}
// Don't render other provider types
return false
})
if (typeof document !== "undefined" && theme.buttonText) {
document.documentElement.style.setProperty(
"--button-text-color",
theme.buttonText
)
}
if (typeof document !== "undefined" && theme.brandColor) {
document.documentElement.style.setProperty(
"--brand-color",
theme.brandColor
)
}
const errors: Record<SignInErrorTypes, string> = {
Signin: "Try signing in with a different account.",
OAuthSignin: "Try signing in with a different account.",
OAuthCallback: "Try signing in with a different account.",
OAuthCreateAccount: "Try signing in with a different account.",
EmailCreateAccount: "Try signing in with a different account.",
Callback: "Try signing in with a different account.",
OAuthAccountNotLinked:
"To confirm your identity, sign in with the same account you used originally.",
EmailSignin: "The e-mail could not be sent.",
CredentialsSignin:
"Sign in failed. Check the details you provided are correct.",
SessionRequired: "Please sign in to access this page.",
default: "Unable to sign in.",
}
const error = errorType && (errors[errorType] ?? errors.default)
const logos = "https://authjs.dev/img/providers"
return (
<div className="signin">
{theme.brandColor && (
<style
dangerouslySetInnerHTML={{
__html: `
:root {
--brand-color: ${theme.brandColor}
}
`,
}}
/>
)}
{theme.buttonText && (
<style
dangerouslySetInnerHTML={{
__html: `
:root {
--button-text-color: ${theme.buttonText}
}
`,
}}
/>
)}
<div className="card">
{theme.logo && <img src={theme.logo} alt="Logo" className="logo" />}
{error && (
<div className="error">
<p>{error}</p>
</div>
)}
{providersToRender.map((provider, i: number) => (
<div key={provider.id} className="provider">
{provider.type === "oauth" && (
<form action={provider.signinUrl} method="POST">
<input type="hidden" name="csrfToken" value={csrfToken} />
{callbackUrl && (
<input type="hidden" name="callbackUrl" value={callbackUrl} />
)}
<button
type="submit"
className="button"
style={
// eslint-disable-next-line
{
"--provider-bg": provider.style?.bg ?? "",
"--provider-dark-bg": provider.style?.bgDark ?? "",
"--provider-color": provider.style?.text ?? "",
"--provider-dark-color": provider.style?.textDark ?? "",
} as React.CSSProperties
}
>
{provider.style?.logo && (
<img
loading="lazy"
height={24}
width={24}
id="provider-logo"
src={`${
provider.style.logo.startsWith("/") ? logos : ""
}${provider.style.logo}`}
/>
)}
{provider.style?.logoDark && (
<img
loading="lazy"
height={24}
width={24}
id="provider-logo-dark"
src={`${
provider.style.logo.startsWith("/") ? logos : ""
}${provider.style.logoDark}`}
/>
)}
<span>Sign in with {provider.name}</span>
</button>
</form>
)}
{(provider.type === "email" || provider.type === "credentials") &&
i > 0 &&
providersToRender[i - 1].type !== "email" &&
providersToRender[i - 1].type !== "credentials" && <hr />}
{provider.type === "email" && (
<form action={provider.signinUrl} method="POST">
<input type="hidden" name="csrfToken" value={csrfToken} />
<label
className="section-header"
htmlFor={`input-email-for-${provider.id}-provider`}
>
Email
</label>
<input
id={`input-email-for-${provider.id}-provider`}
autoFocus
type="email"
name="email"
value={email}
placeholder="email@example.com"
required
/>
<button id="submitButton" type="submit">Sign in with {provider.name}</button>
</form>
)}
{provider.type === "credentials" && (
<form action={provider.callbackUrl} method="POST">
<input type="hidden" name="csrfToken" value={csrfToken} />
{Object.keys(provider.credentials).map((credential) => {
return (
<div key={`input-group-${provider.id}`}>
<label
className="section-header"
htmlFor={`input-${credential}-for-${provider.id}-provider`}
>
{provider.credentials[credential].label ?? credential}
</label>
<input
name={credential}
id={`input-${credential}-for-${provider.id}-provider`}
type={provider.credentials[credential].type ?? "text"}
placeholder={
provider.credentials[credential].placeholder ?? ""
}
{...provider.credentials[credential]}
/>
</div>
)
})}
<button type="submit">Sign in with {provider.name}</button>
</form>
)}
{(provider.type === "email" || provider.type === "credentials") &&
i + 1 < providersToRender.length && <hr />}
</div>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,48 @@
import { Theme } from "../.."
import { InternalUrl } from "../../utils/parse-url"
export interface SignoutProps {
url: InternalUrl
csrfToken: string
theme: Theme
}
export default function SignoutPage(props: SignoutProps) {
const { url, csrfToken, theme } = props
return (
<div className="signout">
{theme.brandColor && (
<style
dangerouslySetInnerHTML={{
__html: `
:root {
--brand-color: ${theme.brandColor}
}
`,
}}
/>
)}
{theme.buttonText && (
<style
dangerouslySetInnerHTML={{
__html: `
:root {
--button-text-color: ${theme.buttonText}
}
`,
}}
/>
)}
<div className="card">
{theme.logo && <img src={theme.logo} alt="Logo" className="logo" />}
<h1>Signout</h1>
<p>Are you sure you want to sign out?</p>
<form action={`${url}/signout`} method="POST">
<input type="hidden" name="csrfToken" value={csrfToken} />
<button id="submitButton" type="submit">Sign out</button>
</form>
</div>
</div>
)
}

View File

@@ -0,0 +1,37 @@
import { Theme } from "../.."
import { InternalUrl } from "../../utils/parse-url"
interface VerifyRequestPageProps {
url: InternalUrl
theme: Theme
}
export default function VerifyRequestPage(props: VerifyRequestPageProps) {
const { url, theme } = props
return (
<div className="verify-request">
{theme.brandColor && (
<style
dangerouslySetInnerHTML={{
__html: `
:root {
--brand-color: ${theme.brandColor}
}
`,
}}
/>
)}
<div className="card">
{theme.logo && <img src={theme.logo} alt="Logo" className="logo" />}
<h1>Check your email</h1>
<p>A sign in link has been sent to your email address.</p>
<p>
<a className="site" href={url.origin}>
{url.host}
</a>
</p>
</div>
</div>
)
}

Some files were not shown because too many files have changed in this diff Show More