├── .gitignore
├── README.md
├── functions
├── auth-callback.js
├── auth-start.js
├── package.json
└── utils
│ ├── auth.js
│ └── netlify-api.js
├── netlify.toml
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── App.css
├── App.js
├── assets
│ └── netlify-login-button.svg
├── components
│ └── ForkMe
│ │ ├── ForkMe.css
│ │ └── index.js
├── index.css
├── index.js
└── utils
│ ├── auth.js
│ └── sort.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 | package-lock.json
6 | /.pnp
7 | .pnp.js
8 |
9 | # testing
10 | /coverage
11 |
12 | # production
13 | /build
14 | .netlify
15 |
16 |
17 | # misc
18 | misc.md
19 | src/stub.js
20 | .DS_Store
21 | .env.local
22 | .env.development.local
23 | .env.test.local
24 | .env.production.local
25 |
26 | npm-debug.log*
27 | yarn-debug.log*
28 | yarn-error.log*
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Netlify OAuth
2 |
3 |
4 |
5 |
6 |
7 |
8 | Example of how to use Netlify OAuth Applications
9 |
10 | ## Use cases
11 |
12 | Using Netlify OAuth you can create custom experiences using the [Netlify Open API](https://open-api.netlify.com/#/default).
13 |
14 | **Here are some use cases:**
15 |
16 | - Building a custom Netlify admin UI
17 | - Building Netlify Desktop Applications
18 | - Making an App that user's existing Netlify sites
19 | - Manage Netlify sites, new deployments, & other things from inside your third party application
20 |
21 | ## Video
22 |
23 |
24 |
25 | ## How it works
26 |
27 | 
28 |
29 | ## Setup
30 |
31 | 1. **Create and Deploy a new Netlify site**
32 |
33 | You can use an [this repo](https://app.netlify.com/start/deploy?repository=https://github.com/netlify-labs/oauth-example)
34 |
35 | 2. **[Create OAuth application](https://app.netlify.com/account/applications)**
36 |
37 | Create your OAuth application in the Netlify admin UI.
38 |
39 | Add in your callback URL. This can be changed later.
40 |
41 | 
42 |
43 | 3. **After creating your OAuth app, Click on show credentials**
44 |
45 | Save these credentials for the next step
46 |
47 | 
48 |
49 | 4. **Take your OAuth credentials and add them to your OAuth app site**
50 |
51 | Set `NETLIFY_OAUTH_CLIENT_ID` and `NETLIFY_OAUTH_CLIENT_SECRET` environment variables in your site
52 |
53 | 
54 |
55 | 5. **Then trigger a new deploy**
56 |
57 | 
58 |
59 |
60 | 6. **Visit your site and verify the OAuth flow is working**
61 |
62 |
--------------------------------------------------------------------------------
/functions/auth-callback.js:
--------------------------------------------------------------------------------
1 | const querystring = require('querystring')
2 | const { config, oauth } = require('./utils/auth')
3 | const { getUser } = require('./utils/netlify-api')
4 |
5 | /* Function to handle netlify auth callback */
6 | exports.handler = async (event, context) => {
7 | // Exit early
8 | if (!event.queryStringParameters) {
9 | return {
10 | statusCode: 401,
11 | body: JSON.stringify({
12 | error: 'Not authorized',
13 | })
14 | }
15 | }
16 |
17 | /* Grant the grant code */
18 | const code = event.queryStringParameters.code
19 | /* state helps mitigate CSRF attacks & Restore the previous state of your app */
20 | const state = querystring.parse(event.queryStringParameters.state)
21 |
22 | try {
23 | /* Take the grant code and exchange for an accessToken */
24 | const authorizationToken = await oauth.authorizationCode.getToken({
25 | code: code,
26 | redirect_uri: config.redirect_uri,
27 | client_id: config.clientId,
28 | client_secret: config.clientSecret
29 | })
30 |
31 | const authResult = oauth.accessToken.create(authorizationToken)
32 |
33 | const token = authResult.token.access_token
34 |
35 | const user = await getUser(token)
36 |
37 | // return {
38 | // statusCode: 200,
39 | // body: JSON.stringify({
40 | // user: user,
41 | // authResult: authResult,
42 | // state: state,
43 | // encode: Buffer.from(token, 'binary').toString('base64')
44 | // })
45 | // }
46 |
47 | const encodedUserData = querystring.stringify({
48 | email: user.email || "NA",
49 | full_name: user.full_name || "NA",
50 | avatar: user.avatar_url || "NA"
51 | })
52 |
53 | const URI = `${state.url}#${encodedUserData}&csrf=${state.csrf}&token=${Buffer.from(token, 'binary').toString('base64')}`
54 | console.log('URI', URI)
55 | /* Redirect user to authorizationURI */
56 | return {
57 | statusCode: 302,
58 | headers: {
59 | Location: URI,
60 | 'Cache-Control': 'no-cache' // Disable caching of this response
61 | },
62 | body: '' // return body for local dev
63 | }
64 |
65 |
66 | } catch (e) {
67 | console.log('Access Token Error', e.message)
68 | console.log(e)
69 | return {
70 | statusCode: e.statusCode || 500,
71 | body: JSON.stringify({
72 | error: e.message,
73 | })
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/functions/auth-start.js:
--------------------------------------------------------------------------------
1 | const { config, oauth } = require('./utils/auth')
2 |
3 | /* Do initial auth redirect */
4 | exports.handler = async (event, context) => {
5 |
6 | if (!event.queryStringParameters) {
7 | return {
8 | statusCode: 401,
9 | body: JSON.stringify({
10 | error: 'No token found',
11 | })
12 | }
13 | }
14 |
15 | const csrfToken = event.queryStringParameters.csrf
16 | const redirectUrl = event.queryStringParameters.url
17 |
18 | /* Generate authorizationURI */
19 | const authorizationURI = oauth.authorizationCode.authorizeURL({
20 | redirect_uri: config.redirect_uri,
21 | /* Specify how your app needs to access the user’s account. */
22 | scope: '',
23 | /* State helps mitigate CSRF attacks & Restore the previous state of your app */
24 | state: `url=${redirectUrl}&csrf=${csrfToken}`,
25 | })
26 |
27 | /* Redirect user to authorizationURI */
28 | return {
29 | statusCode: 302,
30 | headers: {
31 | Location: authorizationURI,
32 | 'Cache-Control': 'no-cache' // Disable caching of this response
33 | },
34 | body: '' // return body for local dev
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/functions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "backend-functions",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "netlify": "^2.3.1",
7 | "node-fetch": "^2.3.0",
8 | "simple-oauth2": "^2.2.1"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/functions/utils/auth.js:
--------------------------------------------------------------------------------
1 | const simpleOauth = require('simple-oauth2')
2 |
3 | const SITE_URL = process.env.URL || 'http://localhost:3000'
4 |
5 | /* Auth values */
6 | const TOKEN_HOST = 'https://api.netlify.com'
7 | const TOKEN_URL = 'https://api.netlify.com/oauth/token'
8 | const USER_PROFILE_URL = 'https://api.netlify.com/api/v1/user'
9 | const AUTHORIZATION_URL = 'https://app.netlify.com/authorize'
10 | const REDIRECT_URL = `${SITE_URL}/.netlify/functions/auth-callback`
11 |
12 | /* Env key name */
13 | const clientIdKey = 'NETLIFY_OAUTH_CLIENT_ID'
14 | const clientSecretKey = 'NETLIFY_OAUTH_CLIENT_SECRET'
15 |
16 | const config = {
17 | /* values set in terminal session or in netlify environment variables */
18 | clientId: process.env[clientIdKey],
19 | clientSecret: process.env[clientSecretKey],
20 | /* OAuth API endpoints */
21 | tokenHost: TOKEN_HOST,
22 | authorizePath: AUTHORIZATION_URL,
23 | tokenPath: TOKEN_URL,
24 | profilePath: USER_PROFILE_URL,
25 | /* redirect_uri is the callback url after successful signin */
26 | redirect_uri: REDIRECT_URL,
27 | }
28 |
29 | function authInstance(credentials) {
30 | if (!credentials.client.id) {
31 | throw new Error(`MISSING REQUIRED ENV VARS. Please set ${clientIdKey}`)
32 | }
33 | if (!credentials.client.secret) {
34 | throw new Error(`MISSING REQUIRED ENV VARS. Please set ${clientSecretKey}`)
35 | }
36 | return simpleOauth.create(credentials)
37 | }
38 |
39 | module.exports = {
40 | /* Export config for functions */
41 | config: config,
42 | /* Create oauth2 instance to use in our functions */
43 | oauth: authInstance({
44 | client: {
45 | id: config.clientId,
46 | secret: config.clientSecret
47 | },
48 | auth: {
49 | tokenHost: config.tokenHost,
50 | tokenPath: config.tokenPath,
51 | authorizePath: config.authorizePath
52 | }
53 | })
54 | }
55 |
--------------------------------------------------------------------------------
/functions/utils/netlify-api.js:
--------------------------------------------------------------------------------
1 | const fetch = require('node-fetch')
2 |
3 | async function getUser(token) {
4 | const url = `https://api.netlify.com/api/v1/user/`
5 | const response = await fetch(url, {
6 | method: 'GET',
7 | headers: {
8 | 'Content-Type': 'application/json',
9 | Authorization: `Bearer ${token}`
10 | }
11 | })
12 |
13 | const data = await response.json()
14 |
15 | if (response.status === 422) {
16 | throw new Error(`Error ${JSON.stringify(data)}`)
17 | }
18 |
19 | return data
20 | }
21 |
22 | module.exports = {
23 | getUser: getUser
24 | }
25 |
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | publish = "build"
3 | command = "npm run build"
4 | functions = "functions"
5 |
6 | [template.environment]
7 | NETLIFY_OAUTH_CLIENT_ID = "Your Netlify OAuth Client ID"
8 | NETLIFY_OAUTH_CLIENT_SECRET= "Your Netlify OAuth Client Secret"
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "oauth-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "netlify": "^2.4.0",
7 | "react": "^16.8.3",
8 | "react-dom": "^16.8.3",
9 | "react-scripts": "2.1.5",
10 | "time-ago": "^0.2.1"
11 | },
12 | "scripts": {
13 | "start": "react-scripts start",
14 | "build": "npm-run-all --parallel build:**",
15 | "build:app": "react-scripts build",
16 | "build:functions": "cd functions && npm install",
17 | "deploy": "npm run build && netlify deploy -p"
18 | },
19 | "eslintConfig": {
20 | "extends": "react-app"
21 | },
22 | "browserslist": [
23 | ">0.2%",
24 | "not dead",
25 | "not ie <= 11",
26 | "not op_mini all"
27 | ],
28 | "devDependencies": {
29 | "npm-run-all": "^4.1.5"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netlify-labs/oauth-example/a7987d8417c908be7b3d04c4c5bf4f5d3a378ada/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
15 |
16 |
25 | React App
26 |
27 |
28 |
29 |
30 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .app {
2 | margin: 20px;
3 | margin-top: 30px;
4 | margin-left: 40px;
5 | }
6 | .app h1 {
7 | margin-bottom: 30px;
8 | }
9 | .title-inner {
10 | display: flex;
11 | align-items: center;
12 | }
13 | .title-inner button {
14 | margin-left: 20px;
15 | }
16 |
17 | button {
18 | cursor: pointer;
19 | background-color: #424242;
20 | font-family: inherit;
21 | color: #fff;
22 | display: inline-flex;
23 | align-items: center;
24 | justify-content: center;
25 | font-size: 15px;
26 | border: 1px solid #e9ebeb;
27 | border-bottom-color: #e1e3e3;
28 | border-radius: 4px;
29 | background-color: #fff;
30 | color: rgba(14,30,37,.87);
31 | box-shadow: 0 2px 4px 0 rgba(14,30,37,.12);
32 | transition: all .2s ease;
33 | transition-property: background-color,color,border,box-shadow;
34 | outline: 0;
35 | font-weight: 500;
36 | background: #00ad9f;
37 | color: #fff;
38 | border-color: transparent;
39 | }
40 | .primary-button {
41 | padding: 13px 18px;
42 | }
43 | .primary-button:hover {
44 | background: #00c2b2;
45 | }
46 |
47 | .search {
48 | border-radius: 2px;
49 | font-size: 16px;
50 | padding: 11px 15px;
51 | min-width: 300px;
52 | display: inline-block;
53 | box-shadow: 0 0 0 2px rgba(120,130,152,.25);
54 | border: none;
55 | outline: none;
56 | transition: all .3s ease;
57 | margin: 20px 0;
58 | }
59 | .search:active, .search:focus, .search:hover {
60 | box-shadow: 0 0 0 2px #00ad9f;
61 | }
62 |
63 | .header {
64 | cursor: pointer;
65 | font-weight: bold;
66 | font-size: 16px !important;
67 | }
68 | .item {
69 | text-align: center;
70 | }
71 | .site-wrapper, .site-wrapper-header {
72 | display: flex;
73 | align-items: center;
74 | padding: 10px 0;
75 | padding-left: 20px;
76 | font-size: 14px;
77 | }
78 | .site-wrapper-header {
79 | padding-left: 15px;
80 | }
81 | .site-wrapper:hover {
82 | background: rgba(14,30,37,.05);
83 | }
84 | .site-screenshot, .site-screenshot-header {
85 | height: 64px;
86 | margin: 0 24px 0 0;
87 | min-width: 102.4px;
88 | position: relative;
89 | }
90 | .site-screenshot-header {
91 | height: 30px;
92 | }
93 | .site-screenshot:before {
94 | background: #dadcdd;
95 | bottom: 0;
96 | content: " ";
97 | left: 0;
98 | position: absolute;
99 | right: 0;
100 | top: 0;
101 | }
102 | .site-screenshot a {
103 | display: block;
104 | position: relative;
105 | z-index: 9;
106 | height: 100%;
107 | }
108 | .site-screenshot img {
109 | position: relative;
110 | width: 100%;
111 | width: 102.4px;
112 | border: none;
113 | }
114 | .site-screenshot img[alt]:after {
115 | display: block;
116 | position: absolute;
117 | top: 0;
118 | left: 0;
119 | width: 100%;
120 | height: 100%;
121 | background-color: #dadcdd;
122 | font-weight: 300;
123 | line-height: 2;
124 | text-align: center;
125 | content: '';
126 | }
127 | .site-info {
128 | min-width: 370px;
129 | max-width: 370px;
130 | }
131 | .site-info h2 {
132 | margin: 0px;
133 | margin-bottom: 5px;
134 | }
135 | .site-info a {
136 | text-decoration: none;
137 | }
138 | .site-meta a {
139 | font-size: 14px;
140 | color: rgba(14,30,37,.8);
141 | }
142 | .site-publish-time {
143 | min-width: 150px;
144 | }
145 | .site-create-time {
146 | min-width: 130px;
147 | }
148 | .site-team {
149 | min-width: 200px;
150 | }
151 | .site-functions {
152 | min-width: 100px;
153 | }
154 | .site-repo-link {
155 | font-size: 12px;
156 | }
157 |
158 | @media (max-width: 1400px) {
159 | .site-repo-link {
160 | display: none;
161 | }
162 | }
163 |
164 | @media (max-width: 1150px) {
165 | .site-functions {
166 | display: none;
167 | }
168 | }
169 |
170 | @media (max-width: 1000px) {
171 | .site-team {
172 | display: none;
173 | }
174 | }
175 |
176 | @media (max-width: 830px) {
177 | .site-create-time {
178 | display: none;
179 | }
180 | .search {
181 | appearance: none;
182 | border: 1px solid rgba(120,130,152,.25);
183 | font-size: 16px;
184 | }
185 | .site-screenshot img[alt]:after {
186 | content: none;
187 | }
188 | .site-screenshot, .site-screenshot img {
189 | background: #dadcdd;
190 | min-height: 50px;
191 | min-width: 78px;
192 | width: 78px;
193 | height: 50px;
194 | margin: 0 20px 0 0;
195 | }
196 | .site-info {
197 | min-width: 350px;
198 | max-width: 350px;
199 | }
200 | }
201 |
202 | @media (max-width: 720px) {
203 | .app {
204 | margin: 15px;
205 | margin-top: 20px;
206 | }
207 | .app h1 {
208 | font-size: 22px;
209 | margin-bottom: inherit;
210 | }
211 | .title-inner {
212 | width: 100%;
213 | display: flex;
214 | justify-content: space-between;
215 | }
216 | .title-inner button {
217 | margin-right: 40px;
218 | }
219 | .contents {
220 | overflow: hidden;
221 | }
222 | .contents input {
223 | margin-left: 3px;
224 | }
225 | .site-wrapper, .site-wrapper-header {
226 | padding-left: 10px;
227 | }
228 | .site-publish-time {
229 | display: none;
230 | }
231 | .site-info h2 {
232 | font-size: 18px;
233 | }
234 | .site-meta a {
235 | font-size: 13px;
236 | }
237 | }
238 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import NetlifyAPI from 'netlify'
3 | import timeAgo from 'time-ago'
4 | import { csrfToken, parseHash, removeHash } from './utils/auth'
5 | import {
6 | sortByDate,
7 | sortByPublishDate,
8 | sortByName,
9 | sortByFunctions,
10 | sortByRepo,
11 | matchText
12 | } from './utils/sort'
13 | import ForkMe from './components/ForkMe'
14 | import loginButton from './assets/netlify-login-button.svg'
15 | import './App.css'
16 |
17 | // import stub from './stub'
18 |
19 | export default class App extends Component {
20 | constructor(props, context) {
21 | super(props, context)
22 | console.log('window.location.hash', window.location.hash)
23 | const response = parseHash(window.location.hash)
24 | /* Clear hash */
25 | removeHash()
26 |
27 | /* Protect against csrf (cross site request forgery https://bit.ly/1V1AvZD) */
28 | if (response.token && !localStorage.getItem(response.csrf)) {
29 | alert('Token invalid. Please try to login again')
30 | return
31 | }
32 |
33 | /* Clean up csrfToken */
34 | localStorage.removeItem(response.csrf)
35 |
36 | /* Set initial app state */
37 | this.state = {
38 | user: response,
39 | sites: [],
40 | filterText: '',
41 | loading: false,
42 | sortBy: 'published_at',
43 | sortOrder: 'desc'
44 | }
45 | }
46 | async componentDidMount() {
47 | const { user } = this.state
48 | if (!user.token) return
49 |
50 | /* Set request loading state */
51 | this.setState({
52 | loading: true
53 | })
54 |
55 | /* Fetch sites from netlify API */
56 | const client = new NetlifyAPI(window.atob(user.token))
57 | const sites = await client.listSites({
58 | filter: 'all'
59 | })
60 |
61 | /* Set sites and turn off loading state */
62 | this.setState({
63 | sites: sites,
64 | loading: false
65 | })
66 | }
67 | handleAuth = e => {
68 | e.preventDefault()
69 | const state = csrfToken()
70 | const { location, localStorage } = window
71 | /* Set csrf token */
72 | localStorage.setItem(state, 'true')
73 | /* Do redirect */
74 | const redirectTo = `${location.origin}${location.pathname}`
75 | window.location.href = `/.netlify/functions/auth-start?url=${redirectTo}&csrf=${state}`
76 | }
77 | handleLogout = e => {
78 | e.preventDefault()
79 | window.location.href = `/`
80 | }
81 | handleFilterInput = e => {
82 | this.setState({
83 | filterText: e.target.value
84 | })
85 | }
86 | handleSort = e => {
87 | const { sortOrder } = this.state
88 | if (e.target && e.target.dataset) {
89 | this.setState({
90 | sortBy: e.target.dataset.sort,
91 | // invert sort order
92 | sortOrder: sortOrder === 'desc' ? 'asc' : 'desc'
93 | })
94 | }
95 | }
96 | renderSiteList = () => {
97 | const { sites, filterText, loading, sortBy, sortOrder } = this.state
98 |
99 | if (loading) {
100 | return Loading sites...
101 | }
102 |
103 | let order
104 | if (sortBy === 'published_at') {
105 | order = sortByPublishDate(sortOrder)
106 | } else if (sortBy === 'name' || sortBy === 'account_name') {
107 | order = sortByName(sortBy, sortOrder)
108 | } else if (sortBy === 'updated_at' || sortBy === 'created_at') {
109 | order = sortByDate(sortBy, sortOrder)
110 | } else if (sortBy === 'functions') {
111 | order = sortByFunctions(sortOrder)
112 | } else if (sortBy === 'repo') {
113 | order = sortByRepo(sortOrder)
114 | }
115 |
116 | const sortedSites = sites.sort(order)
117 |
118 | let matchingSites = sortedSites.filter(site => {
119 | // No search query. Show all
120 | if (!filterText) {
121 | return true
122 | }
123 |
124 | const { name, site_id, ssl_url, build_settings } = site
125 | if (
126 | matchText(filterText, name) ||
127 | matchText(filterText, site_id) ||
128 | matchText(filterText, ssl_url)
129 | ) {
130 | return true
131 | }
132 |
133 | // Matches repo url
134 | if (
135 | build_settings &&
136 | build_settings.repo_url &&
137 | matchText(filterText, build_settings.repo_url)
138 | ) {
139 | return true
140 | }
141 |
142 | // no match!
143 | return false
144 | })
145 | .map((site, i) => {
146 | const {
147 | name,
148 | account_name,
149 | account_slug,
150 | admin_url,
151 | ssl_url,
152 | screenshot_url,
153 | created_at
154 | } = site
155 | const published_deploy = site.published_deploy || {}
156 | const functions = published_deploy.available_functions || []
157 | const functionsNames = functions.map(func => func.n).join(', ')
158 | const build_settings = site.build_settings || {}
159 | const { repo_url } = build_settings
160 | const time = published_deploy.published_at ? timeAgo.ago(new Date(published_deploy.published_at).getTime()) : 'NA'
161 | const createdAt = created_at ? timeAgo.ago(new Date(created_at).getTime()) : 'NA'
162 | return (
163 |
164 |
169 |
181 |
190 |
{time}
191 |
202 |
{createdAt}
203 |
212 |
213 | )
214 | })
215 |
216 | if (!matchingSites.length) {
217 | matchingSites = (
218 |
219 |
220 | No '{filterText}' examples found. Clear your search and try again.
221 |
222 |
223 | )
224 | }
225 | return matchingSites
226 | }
227 | render() {
228 | const { user } = this.state
229 |
230 | /* Not logged in. Show login button */
231 | if (user && !user.token) {
232 | return (
233 |
234 |
235 |
Netlify Site Search
236 |
239 |
240 | )
241 | }
242 |
243 | /* Show admin UI */
244 | return (
245 |
246 |
247 |
248 |
249 | Hi {user.full_name || 'Friend'}
250 |
253 |
254 |
255 |
256 |
261 |
262 |
268 | Site Info
269 |
270 |
275 |
281 | Team
282 |
283 |
289 | Last published
290 |
291 |
297 | Functions
298 |
299 |
305 | Created At
306 |
307 |
313 | Repo
314 |
315 |
316 | {this.renderSiteList()}
317 |
318 |
319 | )
320 | }
321 | }
322 |
--------------------------------------------------------------------------------
/src/assets/netlify-login-button.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/ForkMe/ForkMe.css:
--------------------------------------------------------------------------------
1 | .github-corner svg {
2 | height: 80px;
3 | width: 80px;
4 | fill: #151513;
5 | color: #fff;
6 | position: absolute;
7 | top: 0;
8 | border: 0;
9 | right: 0;
10 | }
11 | .github-corner:hover .octo-arm {
12 | animation:octocat-wave 560ms ease-in-out
13 | }
14 | @keyframes octocat-wave {
15 | 0%, 100%{
16 | transform:rotate(0)
17 | }
18 | 20%, 60%{
19 | transform:rotate(-25deg)
20 | }
21 | 40%, 80%{
22 | transform:rotate(10deg)
23 | }
24 | }
25 | @media (max-width: 720px) {
26 | .github-corner svg {
27 | height: 60px;
28 | width: 60px;
29 | }
30 | }
31 | @media (max-width:500px) {
32 | .github-corner:hover .octo-arm {
33 | animation:none
34 | }
35 | .github-corner .octo-arm {
36 | animation:octocat-wave 560ms ease-in-out
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/ForkMe/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './ForkMe.css'
3 |
4 | const ForkMe = ({ url }) => {
5 | return (
6 |
7 |
12 |
13 | )
14 | }
15 |
16 | export default ForkMe
17 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
6 | sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | code {
12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
13 | }
14 |
15 | a {
16 | text-decoration: none;
17 | color: #00ad9f;
18 | }
19 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import './index.css'
4 | import App from './App'
5 |
6 | ReactDOM.render(, document.getElementById('root'))
7 |
--------------------------------------------------------------------------------
/src/utils/auth.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Generate uuid
3 | * @return {String} - uuidv4
4 | */
5 | export function csrfToken() {
6 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
7 | var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8) // eslint-disable-line
8 | return v.toString(16)
9 | })
10 | }
11 |
12 | /**
13 | * Parse url hash
14 | * @param {String} hash - Hash of URL
15 | * @return {Object} parsed key value object
16 | */
17 | export function parseHash(hash) {
18 | if (!hash) return {}
19 | return hash.replace(/^#/, '').split('&').reduce((result, pair) => {
20 | const keyValue = pair.split('=')
21 | result[keyValue[0]] = decode(keyValue[1])
22 | return result
23 | }, {})
24 | }
25 |
26 | /**
27 | * Remove hash from URL
28 | * @return {null}
29 | */
30 | export function removeHash() {
31 | const { history, location } = window
32 | document.location.hash = ''
33 | history.pushState("", document.title, `${location.pathname}${location.search}`)
34 | }
35 |
36 | function decode(s) {
37 | return decodeURIComponent(s).replace(/\+/g, ' ')
38 | }
39 |
--------------------------------------------------------------------------------
/src/utils/sort.js:
--------------------------------------------------------------------------------
1 |
2 | export function matchText(search, text) {
3 | if (!text || !search) {
4 | return false
5 | }
6 | return text.toLowerCase().indexOf(search.toLowerCase()) > -1
7 | }
8 |
9 | export function sortByDate(dateType, order) {
10 | return function (a, b) {
11 | const timeA = new Date(a[dateType]).getTime()
12 | const timeB = new Date(b[dateType]).getTime()
13 | if (order === 'asc') {
14 | return timeA - timeB
15 | }
16 | // default 'desc' descending order
17 | return timeB - timeA
18 | }
19 | }
20 |
21 | const oldNumber = '2012-02-25T22:21:57.581Z'
22 | export function sortByPublishDate(order) {
23 | return function (a, b) {
24 | const timeOne = (!a.published_deploy) ? oldNumber : a.published_deploy.published_at
25 | const timeTwo = (!b.published_deploy) ? oldNumber : b.published_deploy.published_at
26 | const timeA = new Date(timeOne).getTime()
27 | const timeB = new Date(timeTwo).getTime()
28 | if (order === 'asc') {
29 | return timeA - timeB
30 | }
31 | // default 'desc' descending order
32 | return timeB - timeA
33 | }
34 | }
35 |
36 | export function sortByName(key, order) {
37 | return function (a, b) {
38 | if (order === 'asc') {
39 | if (a[key] < b[key]) return -1
40 | if (a[key] > b[key]) return 1
41 | }
42 | if (a[key] > b[key]) return -1
43 | if (a[key] < b[key]) return 1
44 | return 0
45 | }
46 | }
47 |
48 | export function sortByFunctions(order) {
49 | return function (a, b) {
50 | const functionsOne = (!a.published_deploy) ? [] : a.published_deploy.available_functions
51 | const functionsTwo = (!b.published_deploy) ? [] : b.published_deploy.available_functions
52 | if (order === 'desc') {
53 | if (functionsOne.length < functionsTwo.length) return -1
54 | if (functionsOne.length > functionsTwo.length) return 1
55 | }
56 | if (functionsOne.length > functionsTwo.length) return -1
57 | if (functionsOne.length < functionsTwo.length) return 1
58 | return 0
59 | }
60 | }
61 |
62 | export function sortByRepo(order) {
63 | return function (a, b) {
64 | const settingsOne = a.build_settings || { repo_url: 'a' }
65 | const settingsTwo = b.build_settings || { repo_url: 'a' }
66 | if (order === 'asc') {
67 | if (settingsOne.repo_url < settingsTwo.repo_url) return -1
68 | if (settingsOne.repo_url > settingsTwo.repo_url) return 1
69 | }
70 | if (settingsOne.repo_url > settingsTwo.repo_url) return -1
71 | if (settingsOne.repo_url < settingsTwo.repo_url) return 1
72 | return 0
73 | }
74 | }
75 |
--------------------------------------------------------------------------------