Extension boilerplate
11
.babelrc
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
// "@babel/preset-env"
|
||||||
|
"@babel/preset-react"
|
||||||
|
// "react-app"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
// "@babel/plugin-proposal-class-properties",
|
||||||
|
"react-hot-loader/babel"
|
||||||
|
]
|
||||||
|
}
|
||||||
6
.eslintrc
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"extends": "react-app",
|
||||||
|
"globals": {
|
||||||
|
"chrome": "readonly"
|
||||||
|
}
|
||||||
|
}
|
||||||
70
.gitignore
vendored
|
|
@ -1,67 +1,21 @@
|
||||||
env/
|
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
|
|
||||||
.vscode/
|
|
||||||
|
|
||||||
./frontend/App.test.js
|
|
||||||
|
|
||||||
# backened #
|
|
||||||
|
|
||||||
/backend/secret_settings.py
|
|
||||||
|
|
||||||
*.log
|
|
||||||
*.pot
|
|
||||||
*.pyc
|
|
||||||
__pycache__
|
|
||||||
db.sqlite3
|
|
||||||
media
|
|
||||||
|
|
||||||
# Backup files #
|
|
||||||
*.bak
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# frontend
|
|
||||||
/frontend/.env
|
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/frontend/node_modules
|
/node_modules
|
||||||
/frontend/.pnp
|
|
||||||
/frontend/.pnp.js
|
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
/frontend/coverage
|
/coverage
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/frontend/build
|
/build
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
/frontend/.DS_Store
|
.DS_Store
|
||||||
/frontend/.env.local
|
.env.local
|
||||||
/frontend/.env.development.local
|
.env.development.local
|
||||||
/frontend/.env.test.local
|
.env.test.local
|
||||||
/frontend/.env.production.local
|
.env.production.local
|
||||||
|
.history
|
||||||
|
|
||||||
/frontend/npm-debug.log*
|
# secrets
|
||||||
/frontend/yarn-debug.log*
|
secrets.*.js
|
||||||
/frontend/yarn-error.log*
|
|
||||||
|
|
|
||||||
6
.prettierrc
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"requirePragma": false,
|
||||||
|
"arrowParens": "always"
|
||||||
|
}
|
||||||
3
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
}
|
||||||
4
LICENSE
|
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2022 z0ccc
|
Copyright (c) 2019 Michael Xieyang Liu
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
||||||
170
README.md
Normal file → Executable file
|
|
@ -1,77 +1,151 @@
|
||||||
# Vytal
|
<img src="src/assets/img/icon-128.png" width="64"/>
|
||||||
|
|
||||||
Check it out here: https://vytal.io.
|
# Chrome Extension Boilerplate with React 17 and Webpack 5
|
||||||
|
|
||||||
## About
|
[](https://www.npmjs.com/package/chrome-extension-boilerplate-react)
|
||||||
|
[](https://www.npmjs.com/package/chrome-extension-boilerplate-react)
|
||||||
|
[](https://www.npmjs.com/package/chrome-extension-boilerplate-react)
|
||||||
|
|
||||||
Vytal shows you what traces your browser leaves behind while surfing the web. This scan allows you to understand how easy it is to identify and track your browser even while using a VPN or private mode.
|
[](https://david-dm.org/lxieyang/chrome-extension-boilerplate-react)
|
||||||
|
[](https://david-dm.org/lxieyang/chrome-extension-boilerplate-react?type=dev)
|
||||||
|
|
||||||
A device fingerprint will be generated from your data in the form of a hash. You can sign your hash by entering and saving a signature. You can clear cookies, change your IP or use private mode and reload the page to see if your signature remains the same.
|
## Announcements
|
||||||
|
|
||||||
Vytal contains no ads and signup is not required.
|
- **_This boilerplate adopts [Manifest V3](https://developer.chrome.com/docs/extensions/mv3/intro/mv3-overview/)!_** For V2 users, please check out the [manifest-v2](https://github.com/lxieyang/chrome-extension-boilerplate-react/tree/manifest-v2) branch, or use version [3.x](https://www.npmjs.com/package/chrome-extension-boilerplate-react/v/3.3.0).
|
||||||
|
- Check out the [Manifest V3 Migration Guide](https://developer.chrome.com/docs/extensions/mv3/intro/mv3-migration/).
|
||||||
|
- Recently added [devtools](https://developer.chrome.com/docs/extensions/mv3/devtools/) Support! Thanks [GeekaholicLin](https://github.com/lxieyang/chrome-extension-boilerplate-react/issues/17)!
|
||||||
|
- Recently updated from **[React](https://reactjs.org)** ~~16~~ to **17**!
|
||||||
|
- Recently updated from **[Webpack Dev Server](https://webpack.js.org/configuration/dev-server/)** ~~3.x~~ to **4.x** and **[Webpack](https://webpack.js.org/)** ~~4~~ to **5**!
|
||||||
|
- Recently added [TypeScript](https://www.typescriptlang.org/) Support!
|
||||||
|
|
||||||
## Data Tampering
|
## Features
|
||||||
|
|
||||||
The data used to create device fingerprints can be spoofed or tampered with to prevent tracking. There are a variety of methods used to do this including VPNs, browser extensions and built in browser options. Some methods of data tampering can be detected.
|
This is a basic Chrome Extensions boilerplate to help you write modular and modern Javascript code, load CSS easily and [automatic reload the browser on code changes](https://webpack.github.io/docs/webpack-dev-server.html#automatic-refresh).
|
||||||
|
|
||||||
If data tampering is detected then a red circle with an ‘x’ will be displayed next to the data value. If the legitimate value cannot be identified then the data will be discarded and will not be used to generate a fingerprint. Clicking on the table row of the tampered value will bring up a dialog box showing the types of tampering.
|
This boilerplate is updated with:
|
||||||
|
|
||||||
## Types of Tampering
|
- [Chrome Extension Manifest V3](https://developer.chrome.com/docs/extensions/mv3/intro/mv3-overview/)
|
||||||
|
- [React 17](https://reactjs.org)
|
||||||
|
- [Webpack 5](https://webpack.js.org/)
|
||||||
|
- [Webpack Dev Server 4](https://webpack.js.org/configuration/dev-server/)
|
||||||
|
- [React Hot Loader](https://github.com/gaearon/react-hot-loader)
|
||||||
|
- [eslint-config-react-app](https://www.npmjs.com/package/eslint-config-react-app)
|
||||||
|
- [Prettier](https://prettier.io/)
|
||||||
|
- [TypeScript](https://www.typescriptlang.org/)
|
||||||
|
|
||||||
### Failed Navigator.prototype
|
This boilerplate is heavily inspired by and adapted from [https://github.com/samuelsimoes/chrome-extension-webpack-boilerplate](https://github.com/samuelsimoes/chrome-extension-webpack-boilerplate), with additional support for React 17 features, Webpack 5, and Webpack Dev Server 4.
|
||||||
|
|
||||||
`Navigator.prototype[DataType]` returns a value if the data object was tampered with. Otherwise returns an error.
|
Please open up an issue to nudge me to keep the npm packages up-to-date. FYI, it takes time to make different packages with different versions work together nicely.
|
||||||
|
|
||||||
### Failed undefined properties
|
## Installing and Running
|
||||||
|
|
||||||
`Object.getOwnPropertyDescriptor(navigator, [DataType])` returns an object if the data object was tampered with. Otherwise returns ‘undefined’.
|
### Procedures:
|
||||||
|
|
||||||
### Failed Navigator property value
|
1. Check if your [Node.js](https://nodejs.org/) version is >= **14**.
|
||||||
|
2. Clone this repository.
|
||||||
|
3. Change the package's `name`, `description`, and `repository` fields in `package.json`.
|
||||||
|
4. Change the name of your extension on `src/manifest.json`.
|
||||||
|
5. Run `npm install` to install the dependencies.
|
||||||
|
6. Run `npm start`
|
||||||
|
7. Load your extension on Chrome following:
|
||||||
|
1. Access `chrome://extensions/`
|
||||||
|
2. Check `Developer mode`
|
||||||
|
3. Click on `Load unpacked extension`
|
||||||
|
4. Select the `build` folder.
|
||||||
|
8. Happy hacking.
|
||||||
|
|
||||||
`Object.getOwnPropertyDescriptor(Navigator.prototype, [DataType]).value` returns an error if the data object was tampered with. Otherwise returns ‘undefined’.
|
## Structure
|
||||||
|
|
||||||
### Did not match web worker (\_\_\_)
|
All your extension's code must be placed in the `src` folder.
|
||||||
|
|
||||||
Value does not match the value found in a [web worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers). The web worker value is in the brackets.
|
The boilerplate is already prepared to have a popup, an options page, a background page, and a new tab page (which replaces the new tab page of your browser). But feel free to customize these.
|
||||||
|
|
||||||
### Location data doesn't match system data
|
## TypeScript
|
||||||
|
|
||||||
Occurs when the location data from ip address does not match your location data from your system (such as your timezone).
|
This boilerplate now supports TypeScript! The `Options` Page is implemented using TypeScript. Please refer to `src/pages/Options/` for example usages.
|
||||||
|
|
||||||
### VPN/proxy has been detected
|
## Webpack auto-reload and HRM
|
||||||
|
|
||||||
Your ip address is known to be used by proxies or VPNs.
|
To make your workflow much more efficient this boilerplate uses the [webpack server](https://webpack.github.io/docs/webpack-dev-server.html) to development (started with `npm start`) with auto reload feature that reloads the browser automatically every time that you save some file in your editor.
|
||||||
|
|
||||||
### Failed Date.prototype.setDate.toString()
|
You can run the dev mode on other port if you want. Just specify the env var `port` like this:
|
||||||
|
|
||||||
`Failed Date.prototype.setDate.toString()` returns 'function setDate() { [native code] }' if the data object was NOT tampered with.
|
|
||||||
|
|
||||||
### Failed Screen.prototype
|
|
||||||
|
|
||||||
`Screen.prototype[DataType]` returns a value if the data object was tampered with. Otherwise returns an error.
|
|
||||||
|
|
||||||
### Avail width is greater than width
|
|
||||||
|
|
||||||
Happens when the avail width is greater than the normal width (which is impossible).
|
|
||||||
|
|
||||||
### Avail height is greater than height
|
|
||||||
|
|
||||||
Happens when the height width is greater than the normal height (which is impossible).
|
|
||||||
|
|
||||||
## Dev
|
|
||||||
|
|
||||||
This application uses a React frontend and a Django backend that communicates using the Django REST framework.
|
|
||||||
|
|
||||||
Backend Django setup:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
cd backend
|
$ PORT=6002 npm run start
|
||||||
python manage.py runserver
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Frontend React setup:
|
## Content Scripts
|
||||||
|
|
||||||
|
Although this boilerplate uses the webpack dev server, it's also prepared to write all your bundles files on the disk at every code change, so you can point, on your extension manifest, to your bundles that you want to use as [content scripts](https://developer.chrome.com/extensions/content_scripts), but you need to exclude these entry points from hot reloading [(why?)](https://github.com/samuelsimoes/chrome-extension-webpack-boilerplate/issues/4#issuecomment-261788690). To do so you need to expose which entry points are content scripts on the `webpack.config.js` using the `chromeExtensionBoilerplate -> notHotReload` config. Look the example below.
|
||||||
|
|
||||||
|
Let's say that you want use the `myContentScript` entry point as content script, so on your `webpack.config.js` you will configure the entry point and exclude it from hot reloading, like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
…
|
||||||
|
entry: {
|
||||||
|
myContentScript: "./src/js/myContentScript.js"
|
||||||
|
},
|
||||||
|
chromeExtensionBoilerplate: {
|
||||||
|
notHotReload: ["myContentScript"]
|
||||||
|
}
|
||||||
|
…
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
and on your `src/manifest.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["https://www.google.com/*"],
|
||||||
|
"js": ["myContentScript.bundle.js"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Intelligent Code Completion
|
||||||
|
|
||||||
|
Thanks to [@hudidit](https://github.com/lxieyang/chrome-extension-boilerplate-react/issues/4)'s kind suggestions, this boilerplate supports chrome-specific intelligent code completion using [@types/chrome](https://www.npmjs.com/package/@types/chrome).
|
||||||
|
|
||||||
|
## Packing
|
||||||
|
|
||||||
|
After the development of your extension run the command
|
||||||
|
|
||||||
```
|
```
|
||||||
cd frontend
|
$ NODE_ENV=production npm run build
|
||||||
yarn run start
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Now, the content of `build` folder will be the extension ready to be submitted to the Chrome Web Store. Just take a look at the [official guide](https://developer.chrome.com/webstore/publish) to more infos about publishing.
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
|
||||||
|
If you are developing an extension that talks with some API you probably are using different keys for testing and production. Is a good practice you not commit your secret keys and expose to anyone that have access to the repository.
|
||||||
|
|
||||||
|
To this task this boilerplate import the file `./secrets.<THE-NODE_ENV>.js` on your modules through the module named as `secrets`, so you can do things like this:
|
||||||
|
|
||||||
|
_./secrets.development.js_
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default { key: '123' };
|
||||||
|
```
|
||||||
|
|
||||||
|
_./src/popup.js_
|
||||||
|
|
||||||
|
```js
|
||||||
|
import secrets from 'secrets';
|
||||||
|
ApiCall({ key: secrets.key });
|
||||||
|
```
|
||||||
|
|
||||||
|
:point_right: The files with name `secrets.*.js` already are ignored on the repository.
|
||||||
|
|
||||||
|
## Resources:
|
||||||
|
|
||||||
|
- [Webpack documentation](https://webpack.js.org/concepts/)
|
||||||
|
- [Chrome Extension documentation](https://developer.chrome.com/extensions/getstarted)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Michael Xieyang Liu | [Website](https://lxieyang.github.io)
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
"""
|
|
||||||
ASGI config for backend project.
|
|
||||||
|
|
||||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from django.core.asgi import get_asgi_application
|
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
|
|
||||||
|
|
||||||
application = get_asgi_application()
|
|
||||||
|
|
@ -1,146 +0,0 @@
|
||||||
"""
|
|
||||||
Django settings for backend project.
|
|
||||||
|
|
||||||
Generated by 'django-admin startproject' using Django 3.2.5.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/3.2/topics/settings/
|
|
||||||
|
|
||||||
For the full list of settings and their values, see
|
|
||||||
https://docs.djangoproject.com/en/3.2/ref/settings/
|
|
||||||
"""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from secret_settings import *
|
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
||||||
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
|
||||||
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
|
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
|
||||||
DEBUG = True
|
|
||||||
|
|
||||||
ALLOWED_HOSTS = []
|
|
||||||
|
|
||||||
# Application definition
|
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
|
||||||
'django.contrib.admin',
|
|
||||||
'django.contrib.auth',
|
|
||||||
'django.contrib.contenttypes',
|
|
||||||
'django.contrib.sessions',
|
|
||||||
'django.contrib.messages',
|
|
||||||
'django.contrib.staticfiles',
|
|
||||||
'corsheaders',
|
|
||||||
'rest_framework',
|
|
||||||
'django_filters',
|
|
||||||
'vytal',
|
|
||||||
]
|
|
||||||
|
|
||||||
MIDDLEWARE = [
|
|
||||||
'django.middleware.security.SecurityMiddleware',
|
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
|
||||||
'django.middleware.common.CommonMiddleware',
|
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
||||||
'corsheaders.middleware.CorsMiddleware',
|
|
||||||
]
|
|
||||||
|
|
||||||
ROOT_URLCONF = 'backend.urls'
|
|
||||||
|
|
||||||
TEMPLATES = [
|
|
||||||
{
|
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
|
||||||
'DIRS': [],
|
|
||||||
'APP_DIRS': True,
|
|
||||||
'OPTIONS': {
|
|
||||||
'context_processors': [
|
|
||||||
'django.template.context_processors.debug',
|
|
||||||
'django.template.context_processors.request',
|
|
||||||
'django.contrib.auth.context_processors.auth',
|
|
||||||
'django.contrib.messages.context_processors.messages',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
WSGI_APPLICATION = 'backend.wsgi.application'
|
|
||||||
|
|
||||||
|
|
||||||
# Database
|
|
||||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
|
|
||||||
|
|
||||||
DATABASES = {
|
|
||||||
'default': {
|
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
|
||||||
'NAME': BASE_DIR / 'db.sqlite3',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Password validation
|
|
||||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
|
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
|
||||||
'DEFAULT_RENDERER_CLASSES': [
|
|
||||||
'rest_framework.renderers.JSONRenderer',
|
|
||||||
],
|
|
||||||
'DEFAULT_THROTTLE_CLASSES': [
|
|
||||||
'rest_framework.throttling.AnonRateThrottle',
|
|
||||||
'rest_framework.throttling.UserRateThrottle'
|
|
||||||
],
|
|
||||||
'DEFAULT_THROTTLE_RATES': {
|
|
||||||
'anon': '100/day',
|
|
||||||
'user': '1000/day'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Internationalization
|
|
||||||
# https://docs.djangoproject.com/en/3.2/topics/i18n/
|
|
||||||
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
|
||||||
|
|
||||||
TIME_ZONE = 'UTC'
|
|
||||||
|
|
||||||
USE_I18N = True
|
|
||||||
|
|
||||||
USE_L10N = True
|
|
||||||
|
|
||||||
USE_TZ = True
|
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
|
||||||
# https://docs.djangoproject.com/en/3.2/howto/static-files/
|
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
|
||||||
|
|
||||||
# Default primary key field type
|
|
||||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
|
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
|
||||||
|
|
||||||
CORS_ORIGIN_WHITELIST = [
|
|
||||||
'https://vytal.io',
|
|
||||||
'https://z0ccc.github.io',
|
|
||||||
'http://localhost:3000'
|
|
||||||
]
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
from django.urls import path, include
|
|
||||||
from rest_framework import routers
|
|
||||||
from vytal import views
|
|
||||||
from secret_settings import *
|
|
||||||
|
|
||||||
router = routers.DefaultRouter()
|
|
||||||
router.register(r'fingerprint', views.FingerprintView, 'fingerprint')
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path(ADMIN_URL + '/', admin.site.urls),
|
|
||||||
path('', include(router.urls)),
|
|
||||||
path('', include('vytal.urls')),
|
|
||||||
]
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
"""
|
|
||||||
WSGI config for backend project.
|
|
||||||
|
|
||||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
|
|
||||||
|
|
||||||
application = get_wsgi_application()
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
"""Django's command-line utility for administrative tasks."""
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Run administrative tasks."""
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
|
|
||||||
try:
|
|
||||||
from django.core.management import execute_from_command_line
|
|
||||||
except ImportError as exc:
|
|
||||||
raise ImportError(
|
|
||||||
"Couldn't import Django. Are you sure it's installed and "
|
|
||||||
"available on your PYTHONPATH environment variable? Did you "
|
|
||||||
"forget to activate a virtual environment?"
|
|
||||||
) from exc
|
|
||||||
execute_from_command_line(sys.argv)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
from .models import Fingerprint
|
|
||||||
|
|
||||||
|
|
||||||
class FingerprintAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('name', 'hash')
|
|
||||||
|
|
||||||
# Register your models here.
|
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Fingerprint, FingerprintAdmin)
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class VytalConfig(AppConfig):
|
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
|
||||||
name = 'vytal'
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
# Generated by Django 3.2.5 on 2021-07-18 04:11
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Fingerprint',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(max_length=100)),
|
|
||||||
('hash', models.CharField(max_length=32)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
# Create your models here.
|
|
||||||
|
|
||||||
|
|
||||||
class Fingerprint(models.Model):
|
|
||||||
name = models.CharField(max_length=100)
|
|
||||||
hash = models.CharField(max_length=32)
|
|
||||||
|
|
||||||
def _str_(self):
|
|
||||||
return self.name
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
from rest_framework import serializers
|
|
||||||
from .models import Fingerprint
|
|
||||||
|
|
||||||
|
|
||||||
class FingerprintSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Fingerprint
|
|
||||||
fields = ('id', 'name', 'hash')
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
from django.urls import path
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path('ip/', views.IPView, name='ip'),
|
|
||||||
]
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
from rest_framework import viewsets
|
|
||||||
from .serializers import FingerprintSerializer
|
|
||||||
from .models import Fingerprint
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
|
||||||
from django.http import JsonResponse
|
|
||||||
from ipware import get_client_ip
|
|
||||||
import urllib.request
|
|
||||||
import json
|
|
||||||
from secret_settings import *
|
|
||||||
|
|
||||||
|
|
||||||
class FingerprintView(viewsets.ModelViewSet):
|
|
||||||
serializer_class = FingerprintSerializer
|
|
||||||
queryset = Fingerprint.objects.all()
|
|
||||||
filter_backends = [DjangoFilterBackend]
|
|
||||||
filterset_fields = ['hash']
|
|
||||||
|
|
||||||
|
|
||||||
def IPView(request):
|
|
||||||
ip = get_client_ip(request)
|
|
||||||
with urllib.request.urlopen("https://pro.ip-api.com/json/" + ip[0] + "?key=" + API_KEY) as url:
|
|
||||||
data = json.loads(url.read().decode())
|
|
||||||
return JsonResponse(data)
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
es2021: true,
|
|
||||||
webextensions: true,
|
|
||||||
},
|
|
||||||
extends: ['plugin:react/recommended', 'airbnb'],
|
|
||||||
plugins: ['react'],
|
|
||||||
settings: {
|
|
||||||
'import/resolver': {
|
|
||||||
node: {
|
|
||||||
extensions: ['.js', '.jsx'],
|
|
||||||
moduleDirectory: ['node_modules', 'src/'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'no-plusplus': 'off',
|
|
||||||
'comma-dangle': 'off',
|
|
||||||
'operator-linebreak': 'off',
|
|
||||||
'no-use-before-define': 'off',
|
|
||||||
'linebreak-style': 'off',
|
|
||||||
'react/jsx-filename-extension': [1, { extensions: ['.jsx', '.js'] }],
|
|
||||||
'jsx-a11y/label-has-associated-control': 'off',
|
|
||||||
'one-var': 'off',
|
|
||||||
'one-var-declaration-per-line': 'off',
|
|
||||||
'object-curly-newline': 'off',
|
|
||||||
'implicit-arrow-linebreak': 'off',
|
|
||||||
'import/extensions': [
|
|
||||||
'error',
|
|
||||||
'ignorePackages',
|
|
||||||
{
|
|
||||||
js: 'never',
|
|
||||||
jsx: 'never',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'react/jsx-one-expression-per-line': 'off',
|
|
||||||
'react/prop-types': 'off',
|
|
||||||
'react/react-in-jsx-scope': 'off',
|
|
||||||
'no-bitwise': 'off',
|
|
||||||
'react/no-array-index-key': 'off',
|
|
||||||
'dot-notation': 'off',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
{
|
|
||||||
"homepage": "https://z0ccc.github.io",
|
|
||||||
"name": "vytal",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@fortawesome/fontawesome-pro": "^5.15.3",
|
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.35",
|
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
|
||||||
"@fortawesome/pro-light-svg-icons": "^5.15.3",
|
|
||||||
"@fortawesome/react-fontawesome": "^0.1.14",
|
|
||||||
"axios": "0.21.1",
|
|
||||||
"bowser": "^2.11.0",
|
|
||||||
"crypto-js": "^4.0.0",
|
|
||||||
"emailjs-com": "^3.1.0",
|
|
||||||
"gh-pages": "^3.2.3",
|
|
||||||
"html-react-parser": "^1.2.8",
|
|
||||||
"react": "^17.0.2",
|
|
||||||
"react-dom": "^17.0.2",
|
|
||||||
"react-github-btn": "^1.2.1",
|
|
||||||
"react-modal": "^3.14.3",
|
|
||||||
"react-scripts": "4.0.3",
|
|
||||||
"react-webworker": "^2.1.0",
|
|
||||||
"tslib": "^2.2.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"start": "react-scripts start",
|
|
||||||
"build": "react-scripts build",
|
|
||||||
"test": "react-scripts test",
|
|
||||||
"eject": "react-scripts eject",
|
|
||||||
"predeploy": "yarn run build",
|
|
||||||
"deploy": "echo vytal.io > ./build/CNAME && gh-pages -d build"
|
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"extends": "react-app"
|
|
||||||
},
|
|
||||||
"browserslist": {
|
|
||||||
"production": [
|
|
||||||
">0.2%",
|
|
||||||
"not dead",
|
|
||||||
"not op_mini all"
|
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"eslint": "^7.28.0",
|
|
||||||
"eslint-config-airbnb": "^18.2.1",
|
|
||||||
"eslint-plugin-import": "^2.23.4",
|
|
||||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
|
||||||
"eslint-plugin-react": "^7.24.0",
|
|
||||||
"eslint-plugin-react-hooks": "^4.2.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 786 B |
|
|
@ -1,34 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="theme-color" content="#000000" />
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="Vytal shows you what traces your browser leaves behind while surfing the web."
|
|
||||||
/>
|
|
||||||
<meta name="author" content="z0ccc" />
|
|
||||||
<meta property="og:title" content="Vytal" />
|
|
||||||
<meta property="og:url" content="https://vytal.io" />
|
|
||||||
<meta property="og:img" content="https://vytal.io/vytal.png" />
|
|
||||||
<meta
|
|
||||||
property="og:description"
|
|
||||||
content="Vytal shows you what traces your browser leaves behind while surfing the web."
|
|
||||||
/>
|
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
|
||||||
<meta name="twitter:title" content="Vytal" />
|
|
||||||
<meta
|
|
||||||
name="twitter:description"
|
|
||||||
content="Vytal shows you what traces your browser leaves behind while surfing the web."
|
|
||||||
/>
|
|
||||||
<meta name="twitter:image" content="https://vytal.io/vytal.png" />
|
|
||||||
<link rel="manifest" href="/manifest.json" />
|
|
||||||
<title>Vytal Privacy Scan</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"short_name": "Vytal",
|
|
||||||
"name": "Vytal Privacy Scan",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "favicon.ico",
|
|
||||||
"sizes": "64x64 32x32 24x24 16x16",
|
|
||||||
"type": "image/x-icon"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": ".",
|
|
||||||
"display": "standalone",
|
|
||||||
"theme_color": "#000000",
|
|
||||||
"background_color": "#ffffff"
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
# https://www.robotstxt.org/robotstxt.html
|
|
||||||
User-agent: *
|
|
||||||
Disallow:
|
|
||||||
|
Before Width: | Height: | Size: 167 KiB |
|
|
@ -1,13 +0,0 @@
|
||||||
const data = {
|
|
||||||
locale: Intl.DateTimeFormat().resolvedOptions().locale,
|
|
||||||
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
||||||
timezoneOffset: new Date().getTimezoneOffset(),
|
|
||||||
deviceMemory: navigator.deviceMemory,
|
|
||||||
hardwareConcurrency: navigator.hardwareConcurrency,
|
|
||||||
platform: navigator.platform,
|
|
||||||
userAgent: navigator.userAgent,
|
|
||||||
appVersion: navigator.appVersion,
|
|
||||||
language: navigator.language,
|
|
||||||
};
|
|
||||||
|
|
||||||
postMessage(data);
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
test('renders learn react link', () => {
|
|
||||||
const { getByText } = render(<App />);
|
|
||||||
const linkElement = getByText(/learn react/i);
|
|
||||||
expect(linkElement).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
:root {
|
|
||||||
--main: #943ec5;
|
|
||||||
--grey: #9fa6b2;
|
|
||||||
--text: #4b5563;
|
|
||||||
--border: #ddd;
|
|
||||||
--issueBackground: #f8d7da;
|
|
||||||
--issueText: #721c24;
|
|
||||||
--link: #943ec5;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.App {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.background {
|
|
||||||
position: fixed;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: var(--grey);
|
|
||||||
background: linear-gradient(
|
|
||||||
165deg,
|
|
||||||
rgba(87, 35, 117, 1) 0%,
|
|
||||||
rgba(148, 62, 197, 1) 55%,
|
|
||||||
rgba(211, 176, 231, 1) 100%
|
|
||||||
);
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 0 0 12px 0;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 19px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0 0 4px 0;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 17px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 12px 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
b {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: block;
|
|
||||||
margin: 0 0 12px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
|
||||||
color: var(--link);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 500px) {
|
|
||||||
body {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0 0 3px 0;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import Github from './Github';
|
|
||||||
import MainColumn from './MainColumn';
|
|
||||||
import './App.css';
|
|
||||||
|
|
||||||
const App = () => (
|
|
||||||
<div className="App">
|
|
||||||
<Github />
|
|
||||||
<div className="background" />
|
|
||||||
<MainColumn />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
const ContentBlock = ({ children }) => (
|
|
||||||
<div className="contentBlock">{children}</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default ContentBlock;
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
.centerBlockInner {
|
|
||||||
width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.centerBlockMobile {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contentBlock {
|
|
||||||
color: var(--text);
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 24px;
|
|
||||||
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;
|
|
||||||
margin: 0 0 24px 0;
|
|
||||||
min-width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 1200px) {
|
|
||||||
.centerBlockInner {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.centerBlockMobile {
|
|
||||||
display: block;
|
|
||||||
max-width: 650px;
|
|
||||||
padding: 0 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contentBlock {
|
|
||||||
padding: 18px;
|
|
||||||
margin: 0 0 12px 0;
|
|
||||||
min-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loadBlock {
|
|
||||||
margin: 0 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 500px) {
|
|
||||||
.contentBlock {
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import DataContext from './Context';
|
|
||||||
import BlocksOne from './BlocksOne';
|
|
||||||
import BlocksTwo from './BlocksTwo';
|
|
||||||
// import FontsBlock from './FontsBlock';
|
|
||||||
import { fetchAPI, getWebWorker } from '../utils/common';
|
|
||||||
import './Blocks.css';
|
|
||||||
|
|
||||||
const Blocks = () => {
|
|
||||||
const [workerData, setWorkerData] = useState();
|
|
||||||
const [connectionData, setConnectionData] = useState('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getWebWorker().onmessage = (event) => {
|
|
||||||
setWorkerData(event.data);
|
|
||||||
fetchAPI(setConnectionData);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{connectionData ? (
|
|
||||||
<DataContext.Provider value={{ workerData, connectionData }}>
|
|
||||||
<div className="centerBlockInner">
|
|
||||||
<BlocksOne />
|
|
||||||
</div>
|
|
||||||
<div className="centerBlockInner">
|
|
||||||
<BlocksTwo />
|
|
||||||
</div>
|
|
||||||
<div className="centerBlockMobile">
|
|
||||||
<BlocksOne />
|
|
||||||
<BlocksTwo />
|
|
||||||
</div>
|
|
||||||
</DataContext.Provider>
|
|
||||||
) : (
|
|
||||||
<div className="contentBlock loadBlock">
|
|
||||||
<center>Loading...</center>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Blocks;
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import UserAgentBlock from './UserAgentBlock';
|
|
||||||
import IntlBlock from './IntlBlock';
|
|
||||||
import NavigatorBlock from './NavigatorBlock';
|
|
||||||
import FingerprintBlock from './FingerprintBlock';
|
|
||||||
|
|
||||||
const BlocksOne = () => (
|
|
||||||
<>
|
|
||||||
<FingerprintBlock />
|
|
||||||
<NavigatorBlock />
|
|
||||||
<UserAgentBlock />
|
|
||||||
<IntlBlock />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default BlocksOne;
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import OtherBlock from './OtherBlock';
|
|
||||||
import ScreenBlock from './ScreenBlock';
|
|
||||||
import LocationBlock from './LocationBlock';
|
|
||||||
import ConnectionBlock from './ConnectionBlock';
|
|
||||||
|
|
||||||
const BlocksTwo = () => (
|
|
||||||
<>
|
|
||||||
<LocationBlock />
|
|
||||||
<ConnectionBlock />
|
|
||||||
<ScreenBlock />
|
|
||||||
<OtherBlock />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default BlocksTwo;
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import { useContext } from 'react';
|
|
||||||
import DataContext from './Context';
|
|
||||||
import Block from './Block';
|
|
||||||
import Table from './Table';
|
|
||||||
import { getConnection } from '../utils/connection';
|
|
||||||
|
|
||||||
const LocationBlock = () => {
|
|
||||||
const { connectionData } = useContext(DataContext);
|
|
||||||
return (
|
|
||||||
<Block>
|
|
||||||
<h1>Connection</h1>
|
|
||||||
<Table data={getConnection(connectionData)} />
|
|
||||||
<p>
|
|
||||||
<b>Explanation:</b> Your IP address reveals information about your
|
|
||||||
connection.{' '}
|
|
||||||
<a
|
|
||||||
className="link"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
alt="Read more about ip connection"
|
|
||||||
href="https://en.wikipedia.org/wiki/IP_address"
|
|
||||||
>
|
|
||||||
Read more
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</Block>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LocationBlock;
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import { createContext } from 'react';
|
|
||||||
|
|
||||||
const DataContext = createContext();
|
|
||||||
|
|
||||||
export default DataContext;
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
.fingerprintTable td:first-child {
|
|
||||||
width: 80px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.boxWrap {
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hash {
|
|
||||||
text-align: center;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
margin: 12px 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.saveButton {
|
|
||||||
border: 1px solid var(--grey);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 6px;
|
|
||||||
background-color: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--text);
|
|
||||||
margin: 0 0 0 6px;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.saveButton:hover {
|
|
||||||
background-color: var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type='text'] {
|
|
||||||
border: 1px solid var(--grey);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 6px;
|
|
||||||
width: 200px;
|
|
||||||
outline: none;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 500px) {
|
|
||||||
.boxWrap {
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type='text'] {
|
|
||||||
width: calc(100% - 70px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
import './FingerprintBlock.css';
|
|
||||||
import { useState, useEffect, useContext } from 'react';
|
|
||||||
import DataContext from './Context';
|
|
||||||
import Block from './Block';
|
|
||||||
import Table from './Table';
|
|
||||||
import {
|
|
||||||
getSignature,
|
|
||||||
postSignature,
|
|
||||||
getHash,
|
|
||||||
getFingerprint,
|
|
||||||
} from '../utils/fingerprint';
|
|
||||||
|
|
||||||
const FingerprintBlock = () => {
|
|
||||||
const [signature, setSignature] = useState();
|
|
||||||
const [load, setload] = useState(false);
|
|
||||||
const { workerData } = useContext(DataContext);
|
|
||||||
const hash = getHash(workerData);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getSignature(hash, setSignature, setload);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Block>
|
|
||||||
<h1>Fingerprint</h1>
|
|
||||||
{load && (
|
|
||||||
<>
|
|
||||||
{signature !== undefined ? (
|
|
||||||
<div className="fingerprintTable">
|
|
||||||
<Table data={getFingerprint(signature, hash)} />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="boxWrap">
|
|
||||||
<div className="hash">{hash}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<p>
|
|
||||||
<b>Explanation:</b> A device fingerprint will be generated from your
|
|
||||||
data in the form of a hash. Sign your hash, change your IP or use
|
|
||||||
private mode and reload to see if your signature remains the same.
|
|
||||||
</p>
|
|
||||||
<form onSubmit={(e) => postSignature(hash, e, setSignature)}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="signature"
|
|
||||||
name="signature"
|
|
||||||
placeholder="Enter signature"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="submit"
|
|
||||||
className="saveButton"
|
|
||||||
value="Save"
|
|
||||||
maxLength="100"
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</Block>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FingerprintBlock;
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
import { useEffect } from 'react';
|
|
||||||
import Block from './Block';
|
|
||||||
import fontList from '../fontList.json';
|
|
||||||
|
|
||||||
const FontsBlock = () => {
|
|
||||||
useEffect(() => {
|
|
||||||
const families = ['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy'];
|
|
||||||
const famLen = families.length;
|
|
||||||
const fontsEl = document.querySelector('.fonts');
|
|
||||||
const width = [];
|
|
||||||
const height = [];
|
|
||||||
const span = document.createElement('span');
|
|
||||||
|
|
||||||
span.innerHTML = 'AaBbCcWwLl:/!@的下ㅏㅎ平片';
|
|
||||||
span.style.fontSize = '100px';
|
|
||||||
|
|
||||||
for (let i = 0; i < famLen; i++) {
|
|
||||||
span.style.fontFamily = families[i];
|
|
||||||
fontsEl.appendChild(span);
|
|
||||||
width[i] = span.offsetWidth;
|
|
||||||
height[i] = span.offsetHeight;
|
|
||||||
fontsEl.removeChild(span);
|
|
||||||
}
|
|
||||||
|
|
||||||
function detect(font) {
|
|
||||||
let detected = false;
|
|
||||||
for (let i = 0; i < famLen; i++) {
|
|
||||||
span.style.fontFamily = `"${font}" ,${families[i]}`;
|
|
||||||
fontsEl.appendChild(span);
|
|
||||||
if (span.offsetWidth !== width[i] || span.offsetHeight !== height[i]) {
|
|
||||||
detected = true;
|
|
||||||
}
|
|
||||||
fontsEl.removeChild(span);
|
|
||||||
}
|
|
||||||
return detected;
|
|
||||||
}
|
|
||||||
|
|
||||||
let fontStr = '';
|
|
||||||
fontList.forEach((item) => {
|
|
||||||
if (detect(item.font)) {
|
|
||||||
fontStr += `${item.font}, `;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
fontsEl.textContent = fontStr.slice(0, -2);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Block>
|
|
||||||
<h1>System Fonts</h1>
|
|
||||||
<div className="fonts boxWrap" />
|
|
||||||
<p>
|
|
||||||
<b>Explanation:</b> The fonts you have installed on your computer are
|
|
||||||
generally linked to operating systems, language and system age. Any
|
|
||||||
fonts that you have installed that aren't common for your system
|
|
||||||
can make you easily identifiable.
|
|
||||||
</p>
|
|
||||||
</Block>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FontsBlock;
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
.gitHubButton {
|
|
||||||
position: fixed;
|
|
||||||
top: 12px;
|
|
||||||
right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 900px) {
|
|
||||||
.gitHubButton {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 500px) {
|
|
||||||
.gitHubButton {
|
|
||||||
top: 8px;
|
|
||||||
right: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import './GitHub.css';
|
|
||||||
import GitHubButton from 'react-github-btn';
|
|
||||||
|
|
||||||
const Github = () => (
|
|
||||||
<div className="gitHubButton">
|
|
||||||
<GitHubButton
|
|
||||||
href="https://github.com/z0ccc/Vytal"
|
|
||||||
data-color-scheme="no-preference: light; light: light; dark: light;"
|
|
||||||
aria-label="Star z0ccc/Vytal on GitHub"
|
|
||||||
>
|
|
||||||
Star
|
|
||||||
</GitHubButton>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Github;
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import { useContext } from 'react';
|
|
||||||
import DataContext from './Context';
|
|
||||||
import Block from './Block';
|
|
||||||
import Table from './Table';
|
|
||||||
import getIntl from '../utils/intl';
|
|
||||||
|
|
||||||
const IntlBlock = () => {
|
|
||||||
const { workerData } = useContext(DataContext);
|
|
||||||
return (
|
|
||||||
<Block>
|
|
||||||
<h1>Intl</h1>
|
|
||||||
<Table data={getIntl(workerData)} />
|
|
||||||
<p>
|
|
||||||
<b>Explanation:</b> The Intl object exposes info about your computer.{' '}
|
|
||||||
<a
|
|
||||||
className="link"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
alt="Read more about intl"
|
|
||||||
href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl"
|
|
||||||
>
|
|
||||||
Read more
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</Block>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default IntlBlock;
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import { useContext } from 'react';
|
|
||||||
import DataContext from './Context';
|
|
||||||
import Block from './Block';
|
|
||||||
import Table from './Table';
|
|
||||||
import { getMap, getLocation } from '../utils/connection';
|
|
||||||
|
|
||||||
const LocationBlock = () => {
|
|
||||||
const { workerData, connectionData } = useContext(DataContext);
|
|
||||||
return (
|
|
||||||
<Block>
|
|
||||||
<h1>Location</h1>
|
|
||||||
<img src={getMap(connectionData)} alt="Map of current location" />
|
|
||||||
<Table data={getLocation(connectionData, workerData)} />
|
|
||||||
<p>
|
|
||||||
<b>Explanation:</b> Your IP address can be used to determine your
|
|
||||||
location.{' '}
|
|
||||||
<a
|
|
||||||
className="link"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
alt="Read more about ip location"
|
|
||||||
href="https://en.wikipedia.org/wiki/IP_address"
|
|
||||||
>
|
|
||||||
Read more
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</Block>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LocationBlock;
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
.logoWrap {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
margin: 20px 0 16px 0;
|
|
||||||
width: 250px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 500px) {
|
|
||||||
.logo {
|
|
||||||
width: 160px;
|
|
||||||
margin: 12px 0 8px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import './Logo.css';
|
|
||||||
import { ReactComponent as LogoImg } from '../images/logo.svg';
|
|
||||||
|
|
||||||
const Logo = () => (
|
|
||||||
<div className="logoWrap">
|
|
||||||
<a href="/" className="logo" alt="Vytal logo">
|
|
||||||
<LogoImg />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Logo;
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
.centerBlockOuter {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import './MainColumn.css';
|
|
||||||
import Logo from './Logo';
|
|
||||||
import Blocks from './Blocks';
|
|
||||||
|
|
||||||
const MainColumn = () => (
|
|
||||||
<>
|
|
||||||
<Logo />
|
|
||||||
<div className="centerBlockOuter">
|
|
||||||
<Blocks />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
export default MainColumn;
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import { useContext } from 'react';
|
|
||||||
import DataContext from './Context';
|
|
||||||
import Block from './Block';
|
|
||||||
import Table from './Table';
|
|
||||||
import getNavigator from '../utils/navigator';
|
|
||||||
|
|
||||||
const NavigatorBlock = () => {
|
|
||||||
const { workerData } = useContext(DataContext);
|
|
||||||
return (
|
|
||||||
<Block>
|
|
||||||
<h1>Navigator</h1>
|
|
||||||
<Table data={getNavigator(workerData)} />
|
|
||||||
<p>
|
|
||||||
<b>Explanation:</b> The Navigator interface exposes info about your
|
|
||||||
computer.{' '}
|
|
||||||
<a
|
|
||||||
className="link"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
alt="Read more about navigator"
|
|
||||||
href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator"
|
|
||||||
>
|
|
||||||
Read more
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</Block>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NavigatorBlock;
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
import { useState, useEffect, useContext } from 'react';
|
|
||||||
import DataContext from './Context';
|
|
||||||
import Block from './Block';
|
|
||||||
import Table from './Table';
|
|
||||||
import getOther from '../utils/other';
|
|
||||||
|
|
||||||
const OtherBlock = () => {
|
|
||||||
const [adBlock, setAdBlock] = useState();
|
|
||||||
const [battery, setBattery] = useState();
|
|
||||||
const { workerData } = useContext(DataContext);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch('https://www3.doubleclick.net', {
|
|
||||||
method: 'HEAD',
|
|
||||||
mode: 'no-cors',
|
|
||||||
cache: 'no-store',
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
setAdBlock(false);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setAdBlock(true);
|
|
||||||
});
|
|
||||||
if ('getBattery' in navigator) {
|
|
||||||
navigator.getBattery().then((res) => {
|
|
||||||
setBattery(res);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setBattery('N/A');
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Block>
|
|
||||||
<h1>Other</h1>
|
|
||||||
{battery && adBlock !== undefined && (
|
|
||||||
<Table data={getOther(battery, adBlock, workerData)} />
|
|
||||||
)}
|
|
||||||
</Block>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OtherBlock;
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import Block from './Block';
|
|
||||||
import Table from './Table';
|
|
||||||
import getScreen from '../utils/screen';
|
|
||||||
|
|
||||||
const ScreenBlock = () => (
|
|
||||||
<Block>
|
|
||||||
<h1>Screen</h1>
|
|
||||||
<Table data={getScreen()} />
|
|
||||||
<p>
|
|
||||||
<b>Explanation:</b> The Screen interface exposes info about your computer.{' '}
|
|
||||||
<a
|
|
||||||
className="link"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
alt="Read more about screen"
|
|
||||||
href="https://developer.mozilla.org/en-US/docs/Web/API/Screen"
|
|
||||||
>
|
|
||||||
Read more
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</Block>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default ScreenBlock;
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
table-layout: fixed;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tableWrap {
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody:not(:last-child) {
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import './Table.css';
|
|
||||||
import TableRow from './TableRow';
|
|
||||||
|
|
||||||
const Table = ({ data }) => (
|
|
||||||
<div className="tableWrap">
|
|
||||||
<table>
|
|
||||||
{data.map((item) => (
|
|
||||||
<tbody key={item.key} title={item.code}>
|
|
||||||
<TableRow item={item} />
|
|
||||||
</tbody>
|
|
||||||
))}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Table;
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
td {
|
|
||||||
padding: 12px;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
td:first-child {
|
|
||||||
width: 150px;
|
|
||||||
font-weight: 600;
|
|
||||||
word-break: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
td:nth-child(3) {
|
|
||||||
width: 40px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.circleButton {
|
|
||||||
display: flex;
|
|
||||||
width: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.issue {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.issue:hover {
|
|
||||||
background-color: var(--issueBackground);
|
|
||||||
color: var(--issueText);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modalHeader {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin: 0 0 6px 0;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modalTitle {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.closeButton {
|
|
||||||
fill: var(--border);
|
|
||||||
display: flex;
|
|
||||||
width: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
margin: 0 0 0 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.closeButton:hover {
|
|
||||||
fill: var(--grey);
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
padding-left: 20px;
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 500px) {
|
|
||||||
td {
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
td:first-child {
|
|
||||||
width: 75px;
|
|
||||||
}
|
|
||||||
|
|
||||||
td:nth-child(3) {
|
|
||||||
width: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
word-break: normal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
import './TableRow.css';
|
|
||||||
import Modal from 'react-modal';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { ReactComponent as XCircle } from '../images/xCircle.svg';
|
|
||||||
import { ReactComponent as CheckCircle } from '../images/checkCircle.svg';
|
|
||||||
import { ReactComponent as X } from '../images/x.svg';
|
|
||||||
|
|
||||||
const modalStyles = {
|
|
||||||
content: {
|
|
||||||
top: '50%',
|
|
||||||
left: '50%',
|
|
||||||
right: 'auto',
|
|
||||||
bottom: 'auto',
|
|
||||||
marginRight: '-50%',
|
|
||||||
transform: 'translate(-50%, -50%)',
|
|
||||||
padding: '18px',
|
|
||||||
border: '1px solid var(--border)',
|
|
||||||
borderRadius: '6px',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
Modal.setAppElement('#root');
|
|
||||||
|
|
||||||
const TableRow = ({ item }) => {
|
|
||||||
const issues = item.issues.filter(Boolean).length !== 0;
|
|
||||||
const [modalIsOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
const openModal = () => {
|
|
||||||
if (issues) setIsOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
setIsOpen(false);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<tr className={issues ? 'issue' : ''} onClick={openModal}>
|
|
||||||
<td>{item.key}</td>
|
|
||||||
<td>{item.value || 'N/A'}</td>
|
|
||||||
<td>
|
|
||||||
{issues ? (
|
|
||||||
<XCircle className="circleButton" />
|
|
||||||
) : (
|
|
||||||
<CheckCircle className="circleButton" />
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<Modal
|
|
||||||
isOpen={modalIsOpen}
|
|
||||||
onRequestClose={closeModal}
|
|
||||||
style={modalStyles}
|
|
||||||
contentLabel="Issues Modal"
|
|
||||||
>
|
|
||||||
<div className="modalHeader">
|
|
||||||
<div className="modalTitle">{item.key} issues</div>
|
|
||||||
<X className="closeButton" onClick={closeModal} />
|
|
||||||
</div>
|
|
||||||
<ul>
|
|
||||||
{item.issues.filter(Boolean).map((ele, index) => (
|
|
||||||
<li key={index}>{ele}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TableRow;
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import { useContext } from 'react';
|
|
||||||
import DataContext from './Context';
|
|
||||||
import Block from './Block';
|
|
||||||
import Table from './Table';
|
|
||||||
import getUserAgent from '../utils/userAgent';
|
|
||||||
|
|
||||||
const UserAgentBlock = () => {
|
|
||||||
const { workerData } = useContext(DataContext);
|
|
||||||
return (
|
|
||||||
<Block>
|
|
||||||
<h1>User Agent</h1>
|
|
||||||
<Table data={getUserAgent(workerData.userAgent)} />
|
|
||||||
<p>
|
|
||||||
<b>Explanation:</b> Your user agent can be parsed to determine
|
|
||||||
information about your browser or operating system.{' '}
|
|
||||||
<a
|
|
||||||
className="link"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
alt="Read more about user agent"
|
|
||||||
href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent"
|
|
||||||
>
|
|
||||||
Read more
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</Block>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UserAgentBlock;
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
||||||
<g fill="#9fa6b2">
|
|
||||||
<path d="M464 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h416c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zM32 80c0-8.8 7.2-16 16-16h48v64H32V80zm448 352c0 8.8-7.2 16-16 16H48c-8.8 0-16-7.2-16-16V160h448v272zm0-304H128V64h336c8.8 0 16 7.2 16 16v48z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 360 B |
|
|
@ -1,5 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
||||||
<g fill="#c3e6cb">
|
|
||||||
<path d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 470 B |
|
|
@ -1,5 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
||||||
<g fill="#9fa6b2">
|
|
||||||
<path d="M256 169.92c-28.28.41-52.84 9.62-71.37 28.17-18 18-27.69 41.94-27.28 67.44.78 50.23-3.91 100.5-14 149.39-1.81 8.66 3.78 17.12 12.41 18.91 8.5 1.66 17.09-3.77 18.91-12.44 10.56-51.17 15.5-103.78 14.69-156.35-.25-16.77 6.09-32.5 17.91-44.31 18.66-18.66 42.27-18.8 48.75-18.8 37.12.55 66.41 30.19 66.97 66.06.78 50.37-2.97 100.86-11.12 150.06-1.44 8.72 4.44 16.95 13.16 18.39.91.16 1.78.22 2.66.22 7.69 0 14.47-5.55 15.75-13.39 8.47-51.08 12.34-103.48 11.56-155.79-.87-53-44.81-96.76-99-97.56zm-.09 86.09c-8.84.14-15.87 7.41-15.75 16.25 1.12 73.39-8.22 144.99-27.78 215.07l-1.22 4.33c-1.85 6.63 1.74 20.34 15.41 20.34 7 0 13.41-4.61 15.41-11.66l1.22-4.37c20.41-73.08 30.16-147.74 28.97-224.21-.14-8.84-8.23-15.9-16.26-15.75zM112.66 149.79c-25.19 30.98-38.72 70.11-38.09 110.15.62 39.56-2.62 79.14-9.59 117.61-1.56 8.7 4.19 17.03 12.91 18.61.97.17 1.91.25 2.87.25 7.56 0 14.31-5.42 15.72-13.14 7.34-40.53 10.72-82.18 10.09-123.82-.53-33.01 10.19-63.95 30.91-89.47 5.59-6.86 4.53-16.94-2.31-22.51-6.85-5.55-16.91-4.57-22.51 2.32zm399.22 103.03c-.25-16.5-2.19-33.03-5.75-49.14-1.91-8.61-10.34-14-19.06-12.17-8.62 1.91-14.09 10.44-12.19 19.08 3.09 14 4.78 28.39 5 42.73.12 7.66.16 15.31.09 22.97-.06 8.83 7.03 16.05 15.87 16.12h.12c8.78 0 15.94-7.08 16-15.87.07-7.91.04-15.81-.08-23.72zM252.6.05C182.63-1.29 118.32 24.88 70.32 72.91 24.04 119.22-.87 180.76.16 246.2c.12 7.55.06 15.09-.16 22.62-.25 8.83 6.72 16.2 15.56 16.45H16c8.62 0 15.75-6.87 16-15.55.22-8 .28-16.02.16-24.03-.9-56.69 20.69-110.04 60.78-150.16 41.78-41.84 98.9-64.37 159.15-63.48 74.69 1.09 144.87 38.23 187.75 99.32 5.12 7.23 15.12 8.97 22.28 3.91 7.25-5.08 9-15.05 3.91-22.28C417.28 43.52 337.5 1.3 252.6.05zm1.28 84.93a182.36 182.36 0 0 0-45.19 4.91c-8.59 2.03-13.91 10.66-11.87 19.26 2.03 8.58 10.75 13.87 19.25 11.86 12.12-2.86 25.09-4.25 37.31-4.03 82.78 1.22 151.12 67.56 152.37 147.89.62 39.65-1.34 79.59-5.81 118.7-1 8.78 5.31 16.7 14.09 17.7.62.08 1.22.11 1.84.11 8 0 14.94-6.02 15.87-14.19 4.62-40.47 6.62-81.79 6-122.82-1.49-97.44-83.99-177.92-183.86-179.39z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.1 KiB |
|
|
@ -1,17 +0,0 @@
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
version="1"
|
|
||||||
viewBox="0 0 4389 1056"
|
|
||||||
>
|
|
||||||
<g fill="#fff" stroke="none" transform="matrix(.1 0 0 -.1 0 1056)">
|
|
||||||
<path d="M4890 10554c-19-2-102-11-185-20-814-84-1627-377-2332-841-666-437-1242-1047-1652-1748-345-590-582-1279-670-1950-34-261-44-422-44-710 0-388 28-679 99-1035C523 2160 2153 526 4240 106c360-73 640-99 1040-99 488 0 847 44 1305 160 665 168 1310 477 1860 891 1004 754 1703 1833 1983 3062 146 636 168 1351 61 2010-273 1699-1362 3160-2913 3911-540 261-1091 420-1716 495-100 12-240 17-535 19-220 2-416 1-435-1zm870-978c947-115 1772-494 2460-1130 303-281 615-672 815-1025 491-866 669-1875 504-2861-113-681-390-1322-814-1885-233-310-552-627-869-862-1529-1139-3637-1136-5161 7-391 294-746 669-1012 1070-367 553-599 1170-683 1814-113 869 22 1705 399 2472 103 210 151 294 287 499 316 476 726 886 1199 1199 340 225 629 368 999 495 349 120 681 187 1106 225 112 10 641-3 770-18z"></path>
|
|
||||||
<path d="M5045 8784c-729-59-1353-298-1925-740-149-114-480-445-594-594-405-524-638-1090-722-1755-22-172-25-595-6-760 86-742 352-1362 822-1916 176-207 457-462 565-513 153-73 342-56 483 44 140 98 210 245 199 420-9 156-62 251-211 377-479 408-770 907-876 1504-31 174-39 528-16 719 93 759 503 1435 1121 1847 275 183 609 315 946 372 442 76 912 34 1329-116 593-214 1089-664 1375-1248 194-396 270-726 270-1165-1-320-34-522-135-815-143-414-360-722-786-1116-311-287-141-788 284-834 71-8 180 13 256 51 97 47 353 285 519 482 474 563 751 1223 818 1954 17 188 6 625-19 798-99 656-347 1220-760 1730-118 144-361 385-514 507-520 416-1102 662-1770 748-118 16-543 28-653 19z"></path>
|
|
||||||
<path d="M5175 6514c-92-14-198-39-260-61-458-165-766-607-768-1103-1-309 89-544 306-792 141-160 207-284 259-481 21-80 22-101 27-972l6-890 24-60c79-199 237-332 440-372 282-56 561 117 657 407 17 51 19 120 24 935 6 867 6 881 28 958 51 183 137 343 254 474 207 230 308 486 308 782 0 328-118 612-349 842-171 170-340 263-576 314-69 16-321 28-380 19z"></path>
|
|
||||||
<path d="M39255 8795c-156-35-252-89-375-214-79-79-103-111-137-181-23-47-50-112-60-145-17-57-18-203-21-2915-2-2552-1-2863 13-2933 31-154 97-272 219-392 93-91 176-144 291-183l80-27 1972-3c2216-3 2045-8 2213 74 82 41 109 61 196 148s107 114 147 196c56 113 77 207 77 338-1 222-69 380-232 538-87 85-141 122-228 159-131 55-109 54-1270 57-591 2-1274 4-1517 6l-443 2-2 2433-3 2432-23 73c-86 279-301 480-574 536-92 19-240 20-323 1z"></path>
|
|
||||||
<path d="M12765 8744c-101-17-243-74-332-136-160-109-287-325-313-531-10-84-2-193 21-280 9-37 516-1310 1838-4620 424-1062 439-1096 540-1209 63-71 201-159 301-192 130-43 243-53 364-32 163 27 276 83 399 196 130 120 126 111 585 1263 231 578 735 1842 1120 2807 530 1328 705 1776 718 1840 59 295-87 628-344 784-226 137-480 151-719 41-147-68-257-165-333-294-21-36-377-914-790-1950l-753-1884-756 1890c-416 1039-772 1916-791 1949-48 81-170 202-255 252-114 67-240 104-365 107-58 2-118 1-135-1z"></path>
|
|
||||||
<path d="M26688 8740c-304-52-566-312-619-614-18-104-7-273 24-369 75-231 263-414 512-498l70-24 792-3 793-3 2-2442 3-2442 23-70c87-258 284-451 528-516 108-29 287-29 396-1 270 72 486 299 544 571 11 55 14 457 14 2484v2417h753c474 0 776 4 816 11 164 26 285 91 416 223 151 152 214 307 215 523 0 116-11 179-47 277-66 176-234 350-413 426-140 59 58 54-2465 56-1270 1-2331-2-2357-6z"></path>
|
|
||||||
<path d="M34355 8735c-232-43-433-192-538-402-33-64-2231-5575-2255-5652-13-42-17-90-17-196 1-131 3-146 32-227 78-220 214-370 416-455 112-47 194-64 307-65 143 0 264 32 382 103 119 70 223 181 285 304 12 22 357 879 768 1905s750 1868 754 1872c3 4 339-826 745-1845 406-1018 755-1885 775-1925 133-274 456-451 752-413 109 14 155 29 279 88 79 38 107 59 180 132 71 71 94 103 133 181 89 180 112 363 68 530-22 86-2217 5595-2263 5682-150 281-487 442-803 383z"></path>
|
|
||||||
<path d="M19760 8661c-129-35-236-96-337-194-264-253-298-670-80-966 23-31 467-625 987-1321l945-1265 5-1255 5-1255 24-70c65-194 179-338 343-437 106-63 202-90 343-95 153-6 238 11 364 73 169 83 316 254 379 441l27 78 5 1250 5 1250 1021 1280c561 704 1039 1305 1062 1335 94 127 137 261 135 430-2 240-95 431-283 581-141 113-312 167-495 156-199-13-348-76-478-205-29-28-421-512-872-1075l-819-1025-763 1018c-419 561-781 1040-805 1066-133 145-329 224-548 223-68 0-128-6-170-18z"></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.2 KiB |
|
|
@ -1,5 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512">
|
|
||||||
<g fill="#9fa6b2">
|
|
||||||
<path d="M320 320c-44.18 0-80 35.82-80 80 0 44.19 35.83 80 80 80 44.19 0 80-35.84 80-80 0-44.18-35.82-80-80-80zm0 128c-26.47 0-48-21.53-48-48s21.53-48 48-48 48 21.53 48 48-21.53 48-48 48zm316.21-290.05C459.22-9.9 180.95-10.06 3.79 157.95c-4.94 4.69-5.08 12.51-.26 17.32l5.69 5.69c4.61 4.61 12.07 4.74 16.8.25 164.99-156.39 423.64-155.76 587.97 0 4.73 4.48 12.19 4.35 16.8-.25l5.69-5.69c4.81-4.81 4.67-12.63-.27-17.32zM526.02 270.31c-117.34-104.48-294.86-104.34-412.04 0-5.05 4.5-5.32 12.31-.65 17.2l5.53 5.79c4.46 4.67 11.82 4.96 16.66.67 105.17-93.38 264-93.21 368.98 0 4.83 4.29 12.19 4.01 16.66-.67l5.53-5.79c4.65-4.89 4.38-12.7-.67-17.2z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 746 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 468 B |
|
|
@ -1,5 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
||||||
<g fill="#f4c1c6">
|
|
||||||
<path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm121.6 313.1c4.7 4.7 4.7 12.3 0 17L338 377.6c-4.7 4.7-12.3 4.7-17 0L256 312l-65.1 65.6c-4.7 4.7-12.3 4.7-17 0L134.4 338c-4.7-4.7-4.7-12.3 0-17l65.6-65-65.6-65.1c-4.7-4.7-4.7-12.3 0-17l39.6-39.6c4.7-4.7 12.3-4.7 17 0l65 65.7 65.1-65.6c4.7-4.7 12.3-4.7 17 0l39.6 39.6c4.7 4.7 4.7 12.3 0 17L312 256l65.6 65.1z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 489 B |
|
|
@ -1,16 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import App from './components/App';
|
|
||||||
import * as serviceWorker from './serviceWorker';
|
|
||||||
|
|
||||||
ReactDOM.render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<App />
|
|
||||||
</React.StrictMode>,
|
|
||||||
document.getElementById('root')
|
|
||||||
);
|
|
||||||
|
|
||||||
// If you want your app to work offline and load faster, you can change
|
|
||||||
// unregister() to register() below. Note this comes with some pitfalls.
|
|
||||||
// Learn more about service workers: https://bit.ly/CRA-PWA
|
|
||||||
serviceWorker.unregister();
|
|
||||||
|
|
@ -1,142 +0,0 @@
|
||||||
/* eslint-disable */
|
|
||||||
// This optional code is used to register a service worker.
|
|
||||||
// register() is not called by default.
|
|
||||||
|
|
||||||
// This lets the app load faster on subsequent visits in production, and gives
|
|
||||||
// it offline capabilities. However, it also means that developers (and users)
|
|
||||||
// will only see deployed updates on subsequent visits to a page, after all the
|
|
||||||
// existing tabs open on the page have been closed, since previously cached
|
|
||||||
// resources are updated in the background.
|
|
||||||
|
|
||||||
// To learn more about the benefits of this model and instructions on how to
|
|
||||||
// opt-in, read https://bit.ly/CRA-PWA
|
|
||||||
|
|
||||||
const isLocalhost = Boolean(
|
|
||||||
window.location.hostname === 'localhost' ||
|
|
||||||
// [::1] is the IPv6 localhost address.
|
|
||||||
window.location.hostname === '[::1]' ||
|
|
||||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
|
||||||
window.location.hostname.match(
|
|
||||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
export function register(config) {
|
|
||||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
|
||||||
// The URL constructor is available in all browsers that support SW.
|
|
||||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
|
||||||
if (publicUrl.origin !== window.location.origin) {
|
|
||||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
|
||||||
// from what our page is served on. This might happen if a CDN is used to
|
|
||||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
|
||||||
|
|
||||||
if (isLocalhost) {
|
|
||||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
|
||||||
checkValidServiceWorker(swUrl, config);
|
|
||||||
|
|
||||||
// Add some additional logging to localhost, pointing developers to the
|
|
||||||
// service worker/PWA documentation.
|
|
||||||
navigator.serviceWorker.ready.then(() => {
|
|
||||||
console.log(
|
|
||||||
'This web app is being served cache-first by a service ' +
|
|
||||||
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Is not localhost. Just register service worker
|
|
||||||
registerValidSW(swUrl, config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerValidSW(swUrl, config) {
|
|
||||||
navigator.serviceWorker
|
|
||||||
.register(swUrl)
|
|
||||||
.then((registration) => {
|
|
||||||
registration.onupdatefound = () => {
|
|
||||||
const installingWorker = registration.installing;
|
|
||||||
if (installingWorker == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
installingWorker.onstatechange = () => {
|
|
||||||
if (installingWorker.state === 'installed') {
|
|
||||||
if (navigator.serviceWorker.controller) {
|
|
||||||
// At this point, the updated precached content has been fetched,
|
|
||||||
// but the previous service worker will still serve the older
|
|
||||||
// content until all client tabs are closed.
|
|
||||||
console.log(
|
|
||||||
'New content is available and will be used when all ' +
|
|
||||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Execute callback
|
|
||||||
if (config && config.onUpdate) {
|
|
||||||
config.onUpdate(registration);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// At this point, everything has been precached.
|
|
||||||
// It's the perfect time to display a
|
|
||||||
// "Content is cached for offline use." message.
|
|
||||||
console.log('Content is cached for offline use.');
|
|
||||||
|
|
||||||
// Execute callback
|
|
||||||
if (config && config.onSuccess) {
|
|
||||||
config.onSuccess(registration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Error during service worker registration:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkValidServiceWorker(swUrl, config) {
|
|
||||||
// Check if the service worker can be found. If it can't reload the page.
|
|
||||||
fetch(swUrl, {
|
|
||||||
headers: { 'Service-Worker': 'script' },
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
// Ensure service worker exists, and that we really are getting a JS file.
|
|
||||||
const contentType = response.headers.get('content-type');
|
|
||||||
if (
|
|
||||||
response.status === 404 ||
|
|
||||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
|
||||||
) {
|
|
||||||
// No service worker found. Probably a different app. Reload the page.
|
|
||||||
navigator.serviceWorker.ready.then((registration) => {
|
|
||||||
registration.unregister().then(() => {
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Service worker found. Proceed as normal.
|
|
||||||
registerValidSW(swUrl, config);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
console.log(
|
|
||||||
'No internet connection found. App is running in offline mode.'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unregister() {
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
navigator.serviceWorker.ready
|
|
||||||
.then((registration) => {
|
|
||||||
registration.unregister();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
|
||||||
// allows you to do things like:
|
|
||||||
// expect(element).toHaveTextContent(/react/i)
|
|
||||||
// learn more: https://github.com/testing-library/jest-dom
|
|
||||||
import '@testing-library/jest-dom/extend-expect';
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
// Gets location values
|
|
||||||
const fetchAPI = (setData) => {
|
|
||||||
fetch('https://api.vytal.io/ip/')
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((json) => {
|
|
||||||
setData(json);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkWebWorker = (key, worker) => {
|
|
||||||
if (`${key}` !== `${worker}`) {
|
|
||||||
return `Did not match web worker (${worker})`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getWebWorker = () => {
|
|
||||||
let w;
|
|
||||||
if (typeof w === 'undefined') {
|
|
||||||
w = new Worker('worker.js');
|
|
||||||
}
|
|
||||||
return w;
|
|
||||||
};
|
|
||||||
|
|
||||||
export { fetchAPI, checkWebWorker, getWebWorker };
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
const getMap = (data) =>
|
|
||||||
`https://maps.googleapis.com/maps/api/staticmap?center=${data.lat},${data.lon}&markers=color:red%7Clabel:%7C${data.lat},${data.lon}&size=500x200&zoom=10&key=AIzaSyB-YN-X8PGBSPd7NOaQu4csVhgJUnF3ZGk`;
|
|
||||||
|
|
||||||
const compareTimeZone = (locationTimeZone, workerTimeZone) => {
|
|
||||||
if (locationTimeZone !== workerTimeZone) {
|
|
||||||
return "Location data doesn't match system data";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkProxy = (proxy) => {
|
|
||||||
if (proxy) {
|
|
||||||
return 'VPN/proxy has been detected';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns object with location data
|
|
||||||
const getLocation = (data, workerData) => {
|
|
||||||
const timeZoneIssue = compareTimeZone(data.timezone, workerData.timeZone);
|
|
||||||
const isProxy = checkProxy(data.proxy);
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
key: 'Country',
|
|
||||||
value: data.country,
|
|
||||||
issues: [timeZoneIssue, isProxy],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Region',
|
|
||||||
value: data.regionName,
|
|
||||||
issues: [timeZoneIssue, isProxy],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'City',
|
|
||||||
value: data.city,
|
|
||||||
issues: [timeZoneIssue, isProxy],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Time zone',
|
|
||||||
value: data.timezone,
|
|
||||||
issues: [timeZoneIssue, isProxy],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Zip code',
|
|
||||||
value: data.zip,
|
|
||||||
issues: [timeZoneIssue, isProxy],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Latitude',
|
|
||||||
value: data.lat,
|
|
||||||
issues: [timeZoneIssue, isProxy],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Longitude',
|
|
||||||
value: data.lon,
|
|
||||||
issues: [timeZoneIssue, isProxy],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns object with location data
|
|
||||||
const getConnection = (data) => {
|
|
||||||
const isProxy = checkProxy(data.proxy);
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
key: 'IP address',
|
|
||||||
value: data.query,
|
|
||||||
issues: [isProxy],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'ISP',
|
|
||||||
value: data.isp,
|
|
||||||
issues: [isProxy],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Org',
|
|
||||||
value: data.org,
|
|
||||||
issues: [isProxy],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'ASN',
|
|
||||||
value: data.as,
|
|
||||||
issues: [isProxy],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export { getMap, getConnection, getLocation };
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
import axios from 'axios';
|
|
||||||
import md5 from 'crypto-js/md5';
|
|
||||||
|
|
||||||
const getSignature = (hash, setSignature, setload) => {
|
|
||||||
axios
|
|
||||||
.get(`https://api.vytal.io/fingerprint/?hash=${hash}`)
|
|
||||||
.then((response) => {
|
|
||||||
if (response.data.length !== 0) {
|
|
||||||
setSignature(response.data[response.data.length - 1].name);
|
|
||||||
}
|
|
||||||
setload(true);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const postSignature = (hash, e, setSignature) => {
|
|
||||||
e.preventDefault();
|
|
||||||
axios.post('https://api.vytal.io/fingerprint/', {
|
|
||||||
name: e.target[0].value,
|
|
||||||
hash,
|
|
||||||
});
|
|
||||||
setSignature(e.target[0].value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getHash = (data) => md5(JSON.stringify(data)).toString();
|
|
||||||
|
|
||||||
const getFingerprint = (signature, hash) => [
|
|
||||||
{
|
|
||||||
key: 'Signature',
|
|
||||||
value: signature,
|
|
||||||
issues: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Hash',
|
|
||||||
value: hash,
|
|
||||||
issues: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export { getSignature, postSignature, getHash, getFingerprint };
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
import { checkWebWorker } from './common';
|
|
||||||
|
|
||||||
const getLocale = (locale) => ({
|
|
||||||
key: 'Locale',
|
|
||||||
code: 'Intl.DateTimeFormat().resolvedOptions().locale',
|
|
||||||
value: Intl.DateTimeFormat().resolvedOptions().locale,
|
|
||||||
issues: [
|
|
||||||
checkWebWorker(Intl.DateTimeFormat().resolvedOptions().locale, locale),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const getTimezone = (timeZone) => ({
|
|
||||||
key: 'Timezone',
|
|
||||||
code: 'Intl.DateTimeFormat().resolvedOptions().timeZone',
|
|
||||||
value: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
||||||
issues: [
|
|
||||||
checkWebWorker(Intl.DateTimeFormat().resolvedOptions().timeZone, timeZone),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const getIntl = (workerData) => [
|
|
||||||
getLocale(workerData.locale),
|
|
||||||
getTimezone(workerData.timeZone),
|
|
||||||
];
|
|
||||||
|
|
||||||
export default getIntl;
|
|
||||||
|
|
@ -1,262 +0,0 @@
|
||||||
import { checkWebWorker } from './common';
|
|
||||||
|
|
||||||
const getDeviceMemory = (worker) => {
|
|
||||||
const name = 'deviceMemory';
|
|
||||||
return {
|
|
||||||
key: 'Device memory',
|
|
||||||
code: `navigator.${name}`,
|
|
||||||
value: navigator[name],
|
|
||||||
issues: [
|
|
||||||
checkNavigatorProperties(name),
|
|
||||||
checkNavigatorValue(name),
|
|
||||||
checkNavigatorPrototype(name),
|
|
||||||
checkWebWorker(navigator[name], worker),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getHardwareConcurrency = (worker) => {
|
|
||||||
const name = 'hardwareConcurrency';
|
|
||||||
return {
|
|
||||||
key: 'Hardware concurrency',
|
|
||||||
code: `navigator.${name}`,
|
|
||||||
value: navigator[name],
|
|
||||||
issues: [
|
|
||||||
checkNavigatorProperties(name),
|
|
||||||
checkNavigatorValue(name),
|
|
||||||
checkNavigatorPrototype(name),
|
|
||||||
checkWebWorker(navigator[name], worker),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMaxTouchPoints = () => {
|
|
||||||
const name = 'maxTouchPoints';
|
|
||||||
return {
|
|
||||||
key: 'Max touch points',
|
|
||||||
code: `navigator.${name}`,
|
|
||||||
value: navigator[name],
|
|
||||||
issues: [
|
|
||||||
checkNavigatorProperties(name),
|
|
||||||
checkNavigatorValue(name),
|
|
||||||
checkNavigatorPrototype(name),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPlatform = (worker) => {
|
|
||||||
const name = 'platform';
|
|
||||||
return {
|
|
||||||
key: 'Platform',
|
|
||||||
code: `navigator.${name}`,
|
|
||||||
value: navigator[name],
|
|
||||||
issues: [
|
|
||||||
checkNavigatorProperties(name),
|
|
||||||
checkNavigatorValue(name),
|
|
||||||
checkNavigatorPrototype(name),
|
|
||||||
checkWebWorker(navigator[name], worker),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUserAgent = (worker) => {
|
|
||||||
const name = 'userAgent';
|
|
||||||
return {
|
|
||||||
key: 'User agent',
|
|
||||||
code: `navigator.${name}`,
|
|
||||||
value: navigator[name],
|
|
||||||
issues: [
|
|
||||||
checkNavigatorProperties(name),
|
|
||||||
checkNavigatorValue(name),
|
|
||||||
checkNavigatorPrototype(name),
|
|
||||||
checkWebWorker(navigator[name], worker),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAppVersion = (worker) => {
|
|
||||||
const name = 'appVersion';
|
|
||||||
return {
|
|
||||||
key: 'App version',
|
|
||||||
code: `navigator.${name}`,
|
|
||||||
value: navigator[name],
|
|
||||||
issues: [
|
|
||||||
checkNavigatorProperties(name),
|
|
||||||
checkNavigatorValue(name),
|
|
||||||
checkNavigatorPrototype(name),
|
|
||||||
checkWebWorker(navigator[name], worker),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getLanguage = (worker) => {
|
|
||||||
const name = 'language';
|
|
||||||
return {
|
|
||||||
key: 'Language',
|
|
||||||
code: `navigator.${name}`,
|
|
||||||
value: navigator[name],
|
|
||||||
issues: [
|
|
||||||
checkNavigatorProperties(name),
|
|
||||||
checkNavigatorValue(name),
|
|
||||||
checkNavigatorPrototype(name),
|
|
||||||
checkWebWorker(navigator[name], worker),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getLanguages = () => {
|
|
||||||
const name = 'languages';
|
|
||||||
return {
|
|
||||||
key: 'Languages',
|
|
||||||
code: `navigator.${name}`,
|
|
||||||
value: sortArr(navigator[name]),
|
|
||||||
issues: [
|
|
||||||
checkNavigatorProperties(name),
|
|
||||||
checkNavigatorValue(name),
|
|
||||||
checkNavigatorPrototype(name),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCookieEnabled = () => {
|
|
||||||
const name = 'cookieEnabled';
|
|
||||||
return {
|
|
||||||
key: 'Cookie enabled',
|
|
||||||
code: `navigator.${name}`,
|
|
||||||
value: navigator[name] ? 'True' : 'False',
|
|
||||||
issues: [
|
|
||||||
checkNavigatorProperties(name),
|
|
||||||
checkNavigatorValue(name),
|
|
||||||
checkNavigatorPrototype(name),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDoNotTrack = () => {
|
|
||||||
const name = 'doNotTrack';
|
|
||||||
return {
|
|
||||||
key: 'Do not track',
|
|
||||||
code: `navigator.${name}`,
|
|
||||||
value: navigator[name] ? 'True' : 'False',
|
|
||||||
issues: [
|
|
||||||
checkNavigatorProperties(name),
|
|
||||||
checkNavigatorValue(name),
|
|
||||||
checkNavigatorPrototype(name),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getWebDriver = () => {
|
|
||||||
const name = 'webdriver';
|
|
||||||
return {
|
|
||||||
key: 'Web driver',
|
|
||||||
code: `navigator.${name}`,
|
|
||||||
value: navigator[name] ? 'True' : 'False',
|
|
||||||
issues: [
|
|
||||||
checkNavigatorProperties(name),
|
|
||||||
checkNavigatorValue(name),
|
|
||||||
checkNavigatorPrototype(name),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPlugins = () => {
|
|
||||||
const name = 'plugins';
|
|
||||||
return {
|
|
||||||
key: 'Plugins',
|
|
||||||
code: `navigator.${name}`,
|
|
||||||
value: sortPlugins(navigator[name]),
|
|
||||||
issues: [
|
|
||||||
checkNavigatorProperties(name),
|
|
||||||
checkNavigatorValue(name),
|
|
||||||
checkNavigatorPrototype(name),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getVendor = () => {
|
|
||||||
const name = 'vendor';
|
|
||||||
return {
|
|
||||||
key: 'Vendor',
|
|
||||||
code: `navigator.${name}`,
|
|
||||||
value: navigator[name],
|
|
||||||
issues: [
|
|
||||||
checkNavigatorProperties(name),
|
|
||||||
checkNavigatorValue(name),
|
|
||||||
checkNavigatorPrototype(name),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// sorts array into comma separated list
|
|
||||||
const sortArr = (arr) => {
|
|
||||||
const arrLength = arr.length;
|
|
||||||
let list = '';
|
|
||||||
for (let i = 0; i < arrLength; i++) {
|
|
||||||
if (i !== 0) list += ', ';
|
|
||||||
list += arr[i];
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
};
|
|
||||||
|
|
||||||
// sorts plugins object into comma separated list
|
|
||||||
const sortPlugins = (data) => {
|
|
||||||
const { length } = data;
|
|
||||||
|
|
||||||
let list = '';
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
if (i !== 0) list += ', ';
|
|
||||||
list += data[i].name;
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkNavigatorProperties = (name) => {
|
|
||||||
if (Object.getOwnPropertyDescriptor(navigator, name) !== undefined) {
|
|
||||||
return 'Failed undefined properties';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkNavigatorValue = (name) => {
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const { value } = Object.getOwnPropertyDescriptor(
|
|
||||||
Navigator.prototype,
|
|
||||||
name
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
return 'Failed Navigator property value';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkNavigatorPrototype = (name) => {
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const check = Navigator.prototype[name];
|
|
||||||
return 'Failed Navigator.prototype';
|
|
||||||
} catch (err) {
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const check = '';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getNavigator = (workerData) => [
|
|
||||||
getDeviceMemory(workerData.deviceMemory),
|
|
||||||
getHardwareConcurrency(workerData.hardwareConcurrency),
|
|
||||||
getMaxTouchPoints(),
|
|
||||||
getPlatform(workerData.platform),
|
|
||||||
getUserAgent(workerData.userAgent),
|
|
||||||
getAppVersion(workerData.appVersion),
|
|
||||||
getLanguage(workerData.language),
|
|
||||||
getLanguages(),
|
|
||||||
getCookieEnabled(),
|
|
||||||
getDoNotTrack(),
|
|
||||||
getWebDriver(),
|
|
||||||
getPlugins(),
|
|
||||||
getVendor(),
|
|
||||||
];
|
|
||||||
|
|
||||||
export default getNavigator;
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
import { checkWebWorker } from './common';
|
|
||||||
|
|
||||||
const detectTor = () => {
|
|
||||||
const date = new Date();
|
|
||||||
if (
|
|
||||||
navigator.plugins.length === 0 &&
|
|
||||||
date.getTimezoneOffset() === 0 &&
|
|
||||||
window.outerWidth === window.screen.availWidth &&
|
|
||||||
window.outerHeight === window.screen.availHeight
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkDatePrototype = () => {
|
|
||||||
if (!Date.prototype.setDate.toString().includes('[native code]')) {
|
|
||||||
return 'Failed Date.prototype.setDate.toString()';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns object with location data
|
|
||||||
const getOther = (battery, adBlock, workerData) => {
|
|
||||||
let batteryLevel, batteryStatus;
|
|
||||||
if (battery !== 'N/A') {
|
|
||||||
batteryLevel = `${Math.round(battery.level * 100)}%`;
|
|
||||||
batteryStatus = battery.charging ? 'Charging' : 'Not charging';
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
key: 'Brave browser',
|
|
||||||
code: 'navigator.brave',
|
|
||||||
value: navigator.brave ? 'True' : 'False',
|
|
||||||
issues: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Tor browser',
|
|
||||||
value: detectTor() ? 'True' : 'False',
|
|
||||||
issues: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Adblock',
|
|
||||||
value: adBlock ? 'True' : 'False',
|
|
||||||
issues: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Date',
|
|
||||||
code: 'new Date().toString()',
|
|
||||||
value: new Date().toString(),
|
|
||||||
issues: [checkDatePrototype()],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Timezone offset',
|
|
||||||
code: 'new Date().getTimezoneOffset()',
|
|
||||||
value: new Date().getTimezoneOffset(),
|
|
||||||
issues: [
|
|
||||||
checkDatePrototype(),
|
|
||||||
checkWebWorker(
|
|
||||||
new Date().getTimezoneOffset(),
|
|
||||||
workerData.timezoneOffset
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Battery level',
|
|
||||||
value: batteryLevel,
|
|
||||||
issues: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Battery status',
|
|
||||||
value: batteryStatus,
|
|
||||||
issues: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
export default getOther;
|
|
||||||
|
|
@ -1,161 +0,0 @@
|
||||||
const getWidth = () => {
|
|
||||||
const name = 'width';
|
|
||||||
return {
|
|
||||||
key: 'Width',
|
|
||||||
code: `window.screen.${name}`,
|
|
||||||
value: window.screen[name],
|
|
||||||
issues: [
|
|
||||||
checkScreenProperties('width'),
|
|
||||||
checkScreenValue('width'),
|
|
||||||
checkScreenPrototype('width'),
|
|
||||||
checkWidth(),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAvailWidth = () => {
|
|
||||||
const name = 'availWidth';
|
|
||||||
return {
|
|
||||||
key: 'Avail width',
|
|
||||||
code: `window.screen.${name}`,
|
|
||||||
value: window.screen[name],
|
|
||||||
issues: [
|
|
||||||
checkScreenProperties('availWidth'),
|
|
||||||
checkScreenValue('availWidth'),
|
|
||||||
checkScreenPrototype('availWidth'),
|
|
||||||
checkWidth(),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getOuterWidth = () => {
|
|
||||||
const name = 'outerWidth';
|
|
||||||
return {
|
|
||||||
key: 'Outer width',
|
|
||||||
code: `window.${name}`,
|
|
||||||
value: window[name],
|
|
||||||
issues: [],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getHeight = () => {
|
|
||||||
const name = 'height';
|
|
||||||
return {
|
|
||||||
key: 'Height',
|
|
||||||
code: `window.screen.${name}`,
|
|
||||||
value: window.screen[name],
|
|
||||||
issues: [
|
|
||||||
checkScreenProperties('height'),
|
|
||||||
checkScreenValue('height'),
|
|
||||||
checkScreenPrototype('height'),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAvailHeight = () => {
|
|
||||||
const name = 'availHeight';
|
|
||||||
return {
|
|
||||||
key: 'Avail height',
|
|
||||||
code: `window.screen.${name}`,
|
|
||||||
value: window.screen[name],
|
|
||||||
issues: [
|
|
||||||
checkScreenProperties('availHeight'),
|
|
||||||
checkScreenValue('availHeight'),
|
|
||||||
checkScreenPrototype('availHeight'),
|
|
||||||
checkHeight(),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getOuterHeight = () => {
|
|
||||||
const name = 'outerHeight';
|
|
||||||
return {
|
|
||||||
key: 'Outer height',
|
|
||||||
code: `window.${name}`,
|
|
||||||
value: window[name],
|
|
||||||
issues: [],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPixelDepth = () => {
|
|
||||||
const name = 'pixelDepth';
|
|
||||||
return {
|
|
||||||
key: 'Pixel depth',
|
|
||||||
code: `window.screen.${name}`,
|
|
||||||
value: window.screen[name],
|
|
||||||
issues: [
|
|
||||||
checkScreenProperties('pixelDepth'),
|
|
||||||
checkScreenValue('pixelDepth'),
|
|
||||||
checkScreenPrototype('pixelDepth'),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getColorDepth = () => {
|
|
||||||
const name = 'colorDepth';
|
|
||||||
return {
|
|
||||||
key: 'Color depth',
|
|
||||||
code: `window.screen.${name}`,
|
|
||||||
value: window.screen[name],
|
|
||||||
issues: [
|
|
||||||
checkScreenProperties('colorDepth'),
|
|
||||||
checkScreenValue('colorDepth'),
|
|
||||||
checkScreenPrototype('colorDepth'),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkScreenValue = (name) => {
|
|
||||||
if (
|
|
||||||
Object.getOwnPropertyDescriptor(Screen.prototype, name).value !== undefined
|
|
||||||
) {
|
|
||||||
return 'Failed descriptor.value undefined';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkScreenPrototype = (name) => {
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const check = Screen.prototype[name];
|
|
||||||
return 'Failed Screen.prototype';
|
|
||||||
} catch (err) {
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const check = '';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkWidth = () => {
|
|
||||||
if (window.screen.availWidth > window.screen.width) {
|
|
||||||
return 'Avail width is greater than width';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkHeight = () => {
|
|
||||||
if (window.screen.availHeight > window.screen.height) {
|
|
||||||
return 'Avail height is greater than height';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkScreenProperties = (name) => {
|
|
||||||
if (Object.getOwnPropertyDescriptor(window.screen, name) !== undefined) {
|
|
||||||
return 'Failed undefined properties';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getScreen = () => [
|
|
||||||
getWidth(),
|
|
||||||
getAvailWidth(),
|
|
||||||
getOuterWidth(),
|
|
||||||
getHeight(),
|
|
||||||
getAvailHeight(),
|
|
||||||
getOuterHeight(),
|
|
||||||
getPixelDepth(),
|
|
||||||
getColorDepth(),
|
|
||||||
];
|
|
||||||
|
|
||||||
export default getScreen;
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
import Bowser from 'bowser';
|
|
||||||
import { checkWebWorker } from './common';
|
|
||||||
|
|
||||||
const getUserAgentData = (key, userAgent, workerAgent) => ({
|
|
||||||
key,
|
|
||||||
value: userAgent || 'N/A',
|
|
||||||
issues: [checkWebWorker(userAgent, workerAgent)],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Returns object with location data
|
|
||||||
const getUserAgent = (workerAgent) => {
|
|
||||||
const userAgentParsed = Bowser.parse(navigator.userAgent);
|
|
||||||
const workerAgentParsed = Bowser.parse(workerAgent);
|
|
||||||
return [
|
|
||||||
getUserAgentData(
|
|
||||||
'Browser',
|
|
||||||
userAgentParsed.browser.name,
|
|
||||||
workerAgentParsed.browser.name
|
|
||||||
),
|
|
||||||
getUserAgentData(
|
|
||||||
'Browser version',
|
|
||||||
userAgentParsed.browser.version,
|
|
||||||
workerAgentParsed.browser.version
|
|
||||||
),
|
|
||||||
getUserAgentData('OS', userAgentParsed.os.name, workerAgentParsed.os.name),
|
|
||||||
getUserAgentData(
|
|
||||||
'OS version',
|
|
||||||
userAgentParsed.os.versionName,
|
|
||||||
workerAgentParsed.os.versionName
|
|
||||||
),
|
|
||||||
getUserAgentData(
|
|
||||||
'Engine',
|
|
||||||
userAgentParsed.engine.name,
|
|
||||||
workerAgentParsed.engine.name
|
|
||||||
),
|
|
||||||
getUserAgentData(
|
|
||||||
'Engine version',
|
|
||||||
userAgentParsed.engine.version,
|
|
||||||
workerAgentParsed.engine.version
|
|
||||||
),
|
|
||||||
getUserAgentData(
|
|
||||||
'Platform type',
|
|
||||||
userAgentParsed.platform.type,
|
|
||||||
workerAgentParsed.platform.type
|
|
||||||
),
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default getUserAgent;
|
|
||||||
11596
frontend/yarn.lock
19779
package-lock.json
generated
Normal file
58
package.json
Executable file
|
|
@ -0,0 +1,58 @@
|
||||||
|
{
|
||||||
|
"name": "chrome-extension-boilerplate-react",
|
||||||
|
"version": "4.3.5",
|
||||||
|
"description": "A chrome extension boilerplate built with React 17, Webpack 5, and Webpack Dev Server 4",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/lxieyang/chrome-extension-boilerplate-react.git"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "node utils/build.js",
|
||||||
|
"start": "node utils/webserver.js",
|
||||||
|
"prettier": "prettier --write '**/*.{js,jsx,ts,tsx,json,css,scss,md}'"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hot-loader/react-dom": "^17.0.2",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
|
"react-hot-loader": "^4.13.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.17.0",
|
||||||
|
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
||||||
|
"@babel/preset-env": "^7.16.11",
|
||||||
|
"@babel/preset-react": "^7.16.7",
|
||||||
|
"@types/chrome": "^0.0.177",
|
||||||
|
"@types/react": "^17.0.39",
|
||||||
|
"@types/react-dom": "^17.0.11",
|
||||||
|
"babel-eslint": "^10.1.0",
|
||||||
|
"babel-loader": "^8.2.3",
|
||||||
|
"babel-preset-react-app": "^10.0.1",
|
||||||
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
|
"copy-webpack-plugin": "^7.0.0",
|
||||||
|
"css-loader": "^6.6.0",
|
||||||
|
"eslint": "^8.8.0",
|
||||||
|
"eslint-config-react-app": "^7.0.0",
|
||||||
|
"eslint-plugin-flowtype": "^8.0.3",
|
||||||
|
"eslint-plugin-import": "^2.25.4",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||||
|
"eslint-plugin-react": "^7.28.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.3.0",
|
||||||
|
"file-loader": "^6.2.0",
|
||||||
|
"fs-extra": "^10.0.0",
|
||||||
|
"html-loader": "^3.1.0",
|
||||||
|
"html-webpack-plugin": "^5.5.0",
|
||||||
|
"node-sass": "^6.0.1",
|
||||||
|
"prettier": "^2.5.1",
|
||||||
|
"sass-loader": "^12.4.0",
|
||||||
|
"source-map-loader": "^3.0.1",
|
||||||
|
"style-loader": "^3.3.1",
|
||||||
|
"terser-webpack-plugin": "^5.3.1",
|
||||||
|
"ts-loader": "^9.2.6",
|
||||||
|
"typescript": "^4.5.5",
|
||||||
|
"webpack": "^5.68.0",
|
||||||
|
"webpack-cli": "^4.9.2",
|
||||||
|
"webpack-dev-server": "^4.7.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/assets/img/icon-128.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/img/icon-34.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
7
src/assets/img/logo.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
|
||||||
|
<g fill="#61DAFB">
|
||||||
|
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
|
||||||
|
<circle cx="420.9" cy="296.5" r="45.7"/>
|
||||||
|
<path d="M520.5 78.1z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
19
src/containers/Greetings/Greetings.jsx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import icon from '../../assets/img/icon-128.png';
|
||||||
|
|
||||||
|
class GreetingComponent extends Component {
|
||||||
|
state = {
|
||||||
|
name: 'dev',
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>Hello, {this.state.name}!</p>
|
||||||
|
<img src={icon} alt="extension icon" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GreetingComponent;
|
||||||
31
src/manifest.json
Executable file
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Chrome Extension with React & Webpack",
|
||||||
|
"description": "A chrome extension boilerplate built with React 17, Webpack 5, and Webpack Dev Server 4",
|
||||||
|
"options_page": "options.html",
|
||||||
|
"background": { "service_worker": "background.bundle.js" },
|
||||||
|
"action": {
|
||||||
|
"default_popup": "popup.html",
|
||||||
|
"default_icon": "icon-34.png"
|
||||||
|
},
|
||||||
|
"chrome_url_overrides": {
|
||||||
|
"newtab": "newtab.html"
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"128": "icon-128.png"
|
||||||
|
},
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["http://*/*", "https://*/*", "<all_urls>"],
|
||||||
|
"js": ["contentScript.bundle.js"],
|
||||||
|
"css": ["content.styles.css"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"devtools_page": "devtools.html",
|
||||||
|
"web_accessible_resources": [
|
||||||
|
{
|
||||||
|
"resources": ["content.styles.css", "icon-128.png", "icon-34.png"],
|
||||||
|
"matches": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
2
src/pages/Background/index.js
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
console.log('This is the background page.');
|
||||||
|
console.log('Put the background scripts here.');
|
||||||
6
src/pages/Content/index.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { printLine } from './modules/print';
|
||||||
|
|
||||||
|
console.log('Content script works!');
|
||||||
|
console.log('Must reload extension for modifications to take effect.');
|
||||||
|
|
||||||
|
printLine("Using the 'printLine' function from the Print Module");
|
||||||
3
src/pages/Content/modules/print.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const printLine = (line) => {
|
||||||
|
console.log('===> FROM THE PRINT MODULE:', line);
|
||||||
|
};
|
||||||
9
src/pages/Devtools/index.html
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title></title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body></body>
|
||||||
|
</html>
|
||||||
5
src/pages/Devtools/index.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
chrome.devtools.panels.create(
|
||||||
|
'Dev Tools from chrome-extension-boilerplate-react',
|
||||||
|
'icon-34.png',
|
||||||
|
'panel.html'
|
||||||
|
);
|
||||||
38
src/pages/Newtab/Newtab.css
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
.App {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-logo {
|
||||||
|
height: 40vmin;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.App-logo {
|
||||||
|
animation: App-logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-header {
|
||||||
|
background-color: #282c34;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: calc(10px + 2vmin);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-link {
|
||||||
|
color: #61dafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes App-logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/pages/Newtab/Newtab.jsx
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import React from 'react';
|
||||||
|
import logo from '../../assets/img/logo.svg';
|
||||||
|
import './Newtab.css';
|
||||||
|
import './Newtab.scss';
|
||||||
|
|
||||||
|
const Newtab = () => {
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
<header className="App-header">
|
||||||
|
<img src={logo} className="App-logo" alt="logo" />
|
||||||
|
<p>
|
||||||
|
Edit <code>src/pages/Newtab/Newtab.js</code> and save to reload.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
className="App-link"
|
||||||
|
href="https://reactjs.org"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Learn React!
|
||||||
|
</a>
|
||||||
|
<h6>The color of this paragraph is defined using SASS.</h6>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Newtab;
|
||||||
10
src/pages/Newtab/Newtab.scss
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
$myColor: red;
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
color: $myColor;
|
||||||
|
}
|
||||||
13
src/pages/Newtab/index.css
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
11
src/pages/Newtab/index.html
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Chrome Extension Boilerplate (with React 16.6+ & Webpack 4+)</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app-container"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
9
src/pages/Newtab/index.jsx
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { render } from 'react-dom';
|
||||||
|
|
||||||
|
import Newtab from './Newtab';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
render(<Newtab />, window.document.querySelector('#app-container'));
|
||||||
|
|
||||||
|
if (module.hot) module.hot.accept();
|
||||||
8
src/pages/Options/Options.css
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
.OptionsContainer {
|
||||||
|
width: 100%;
|
||||||
|
height: 50vh;
|
||||||
|
font-size: 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
12
src/pages/Options/Options.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import React from 'react';
|
||||||
|
import './Options.css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Options: React.FC<Props> = ({ title }: Props) => {
|
||||||
|
return <div className="OptionsContainer">{title} Page</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Options;
|
||||||