Merge cbad0fd2d1
into f5796c24b3
commit
ea5fe76245
@ -0,0 +1,26 @@
|
||||
name: Publish on Docker Hub
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Login to Docker Hub
|
||||
run: docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_KEY }}
|
||||
|
||||
- name: Build base Sherlock image
|
||||
run: docker build -t sherlock .
|
||||
|
||||
- name: Build and push Sherlock API image
|
||||
run: cd sherlock-web/api &&
|
||||
docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/sherlock-api:latest . &&
|
||||
docker push ${{ secrets.DOCKERHUB_USERNAME }}/sherlock-api:latest
|
||||
|
||||
- name: Build and push sherlock-frontend image
|
||||
run: |
|
||||
docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/sherlock-web:latest -f ./sherlock-web/frontend/Dockerfile . &&
|
||||
docker push ${{ secrets.DOCKERHUB_USERNAME }}/sherlock-web:latest
|
@ -0,0 +1,9 @@
|
||||
FROM sherlock
|
||||
|
||||
RUN pip3 install fastapi[all]
|
||||
|
||||
COPY main.py /app/
|
||||
|
||||
WORKDIR /app/
|
||||
|
||||
ENTRYPOINT ["uvicorn", "main:app", "--host=0.0.0.0", "--port=8000", "--reload"]
|
@ -0,0 +1,58 @@
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Annotated
|
||||
from pydantic import BaseModel
|
||||
|
||||
from fastapi import FastAPI, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
class Body(BaseModel):
|
||||
usernames: list[str]
|
||||
sites: list[str]
|
||||
f: list[str]
|
||||
|
||||
|
||||
@app.post("/")
|
||||
async def root(body: Body):
|
||||
command = ["python3", "/opt/sherlock/sherlock/sherlock.py"]
|
||||
|
||||
usernames = body.usernames
|
||||
sites = body.sites
|
||||
f = body.f
|
||||
|
||||
if usernames:
|
||||
for name in usernames:
|
||||
command.append(name)
|
||||
|
||||
if sites:
|
||||
for site in sites:
|
||||
command.append("--site")
|
||||
command.append(site)
|
||||
|
||||
if f:
|
||||
for flag in f:
|
||||
command.append("--"+flag)
|
||||
|
||||
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=sys.stderr)
|
||||
|
||||
response = []
|
||||
|
||||
for line in iter(process.stdout.readline, b''):
|
||||
line = line.decode("utf-8").rstrip("\n").rstrip("\r")[4:]
|
||||
|
||||
if line == "":
|
||||
continue
|
||||
|
||||
if line.startswith("Checking username"):
|
||||
continue
|
||||
|
||||
if line.startswith("Search complete"):
|
||||
continue
|
||||
|
||||
data = line.split(": ")
|
||||
|
||||
response.append(dict(name= data[0], url= data[1]))
|
||||
|
||||
return response
|
@ -0,0 +1,18 @@
|
||||
services:
|
||||
api:
|
||||
build: ./api
|
||||
volumes:
|
||||
- ./api/main.py:/app/main.py
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: ./sherlock-web/frontend/Dockerfile
|
||||
command: npm run dev
|
||||
volumes:
|
||||
- ./frontend/app:/app/app/
|
||||
- ./frontend/public:/app/public
|
||||
ports:
|
||||
- 3000:3000
|
||||
depends_on:
|
||||
- api
|
@ -0,0 +1,18 @@
|
||||
FROM node:20
|
||||
WORKDIR /app/
|
||||
|
||||
COPY ./sherlock-web/frontend/package.json ./sherlock-web/frontend/package-lock.json ./
|
||||
RUN npm i
|
||||
|
||||
COPY ./sherlock-web/frontend/tsconfig.json ./tsconfig.json
|
||||
COPY ./sherlock-web/frontend/next-env.d.ts ./next-env.d.ts
|
||||
COPY ./sherlock-web/frontend/next.config.mjs ./src/next.config.mjs
|
||||
|
||||
COPY ./sherlock-web/frontend/app/ ./app/
|
||||
COPY ./sherlock-web/frontend/public/ ./public/
|
||||
|
||||
COPY ./sherlock/resources/data.json ./data.json
|
||||
|
||||
RUN npm run build
|
||||
|
||||
CMD npm run start
|
@ -0,0 +1,22 @@
|
||||
export async function POST(req: Request) {
|
||||
const data = await req.json();
|
||||
|
||||
const { username, sites, withNSFW } = data;
|
||||
|
||||
const f = [];
|
||||
|
||||
if (withNSFW)
|
||||
f.push("nsfw");
|
||||
|
||||
return await fetch("http://api:8000", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
usernames: [username],
|
||||
sites,
|
||||
f
|
||||
})
|
||||
});
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
"use client"
|
||||
import { Grid, Input, Divider, Stack, Checkbox, Button, Link, Typography } from "@mui/joy";
|
||||
|
||||
import SiteCheckbox from "./siteCheckbox";
|
||||
|
||||
import * as data from "../../data.json";
|
||||
import { ChangeEvent, FormEvent, useState } from "react";
|
||||
import { pageList } from "../page";
|
||||
|
||||
let sites: pageList = [];
|
||||
let sitesWithNSFW: pageList = [];
|
||||
|
||||
const checkedSitesDefault: Record<string, boolean> = {};
|
||||
|
||||
const dataEntries = Object.entries(data).sort(([a], [b]) => a.toUpperCase() > b.toUpperCase() ? 1 : -1);
|
||||
|
||||
for (const [name, values] of dataEntries) {
|
||||
if (name === "default")
|
||||
continue;
|
||||
|
||||
const {url, isNSFW} = values as any;
|
||||
|
||||
const parsedData = {name, url};
|
||||
|
||||
sitesWithNSFW.push(parsedData);
|
||||
|
||||
checkedSitesDefault[name] = false;
|
||||
|
||||
if (!isNSFW)
|
||||
sites.push(parsedData)
|
||||
}
|
||||
|
||||
type FormProps = {
|
||||
setFoundData: (data: pageList) => void
|
||||
};
|
||||
|
||||
export default function Form({setFoundData}: FormProps) {
|
||||
const [withNSFW, setWithNSFW] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [checkedSites, setCheckedSites] = useState(checkedSitesDefault);
|
||||
|
||||
const clickNSFW = (e: ChangeEvent<HTMLInputElement>) => setWithNSFW(e.target.checked);
|
||||
|
||||
const clickCheckAll = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const {checked} = e.target;
|
||||
|
||||
let newCheckedSites = {...checkedSites};
|
||||
for (let key in checkedSites)
|
||||
newCheckedSites[key] = checked;
|
||||
|
||||
setCheckedSites(newCheckedSites);
|
||||
};
|
||||
|
||||
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const response = await fetch("/api/submit", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: e.currentTarget.username.value,
|
||||
sites: Object.entries(checkedSites).filter(([_, value]) => value).map(([key, _]) => key),
|
||||
withNSFW
|
||||
})
|
||||
});
|
||||
|
||||
let parsedData = await response.json();
|
||||
|
||||
parsedData = parsedData.sort((a: any, b: any) => a.name.toUpperCase() > b.name.toUpperCase() ? 1 : -1);
|
||||
|
||||
parsedData = parsedData.filter((item : any) => (item.url !== "Desired sites not found"));
|
||||
|
||||
setFoundData(parsedData);
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const currentSite = withNSFW ? sitesWithNSFW : sites;
|
||||
|
||||
const required = !Object.values(checkedSites).some(value => value);
|
||||
|
||||
return (
|
||||
<form style={{ height: "95%"}} onSubmit={onSubmit}>
|
||||
<Stack direction="column" spacing={2} sx={{height: "100%"}}>
|
||||
<Input name="username" required />
|
||||
|
||||
<Divider>
|
||||
<Stack direction="row" spacing={3}>
|
||||
<Checkbox label="With NSFW" onChange={clickNSFW} />
|
||||
<Checkbox label="Check All" onChange={clickCheckAll} />
|
||||
</Stack>
|
||||
</Divider>
|
||||
|
||||
<Grid container spacing={2} columns={{ xs: 4, sm: 6, md: 8, lg: 10, xl: 12 }} sx={{maxHeight: "100%", overflow: "auto"}} flexGrow={1}>
|
||||
{currentSite.map(
|
||||
({name, url}) => {
|
||||
const change = (value: boolean) => setCheckedSites({
|
||||
...checkedSites,
|
||||
[name]: value
|
||||
});
|
||||
|
||||
return (
|
||||
<SiteCheckbox
|
||||
name={name}
|
||||
url={url}
|
||||
key={"site-"+name}
|
||||
checked={checkedSites[name]}
|
||||
onChange={change}
|
||||
required={required}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
<Button type="submit" loading={loading}>Send</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
import { Checkbox, Grid, Tooltip } from "@mui/joy"
|
||||
import { ChangeEvent } from "react";
|
||||
|
||||
type props = {
|
||||
name: string,
|
||||
url: string,
|
||||
checked: boolean | undefined,
|
||||
onChange: (value: boolean) => void,
|
||||
required: boolean,
|
||||
}
|
||||
|
||||
export default function SiteCheckBox({name, url, checked, onChange, required}: props) {
|
||||
const change = (e: ChangeEvent<HTMLInputElement>) => onChange(e.target.checked);
|
||||
|
||||
return (
|
||||
<Grid xs={2} sm={2} md={2} lg={2} >
|
||||
<Tooltip title={url}>
|
||||
<Checkbox
|
||||
name={"site["+name+"]"}
|
||||
label={name}
|
||||
checked={checked}
|
||||
onChange={change}
|
||||
required={required}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
);
|
||||
}
|
After Width: | Height: | Size: 25 KiB |
@ -0,0 +1,16 @@
|
||||
html,
|
||||
body,
|
||||
#__next {
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
#__next {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
width: calc(100vw - 5vh);
|
||||
height: 95vh;
|
||||
margin: 2.5vh;
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
declare module '*.json' {
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
export const metadata = {
|
||||
title: 'Sherlock',
|
||||
description: 'Find your username in many websites.',
|
||||
}
|
||||
|
||||
import "../node_modules/reset-css/reset.css";
|
||||
import "./global.css";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Sherlock</title>
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
"use client"
|
||||
import { CssVarsProvider } from '@mui/joy/styles';
|
||||
|
||||
import '../node_modules/reset-css/reset.css';
|
||||
import { Box, Button, Grid, Link, Sheet, Stack, Typography } from '@mui/joy';
|
||||
import Form from './components/form';
|
||||
import { useState } from 'react';
|
||||
|
||||
const mainSheetStyle = {
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
padding: "10px",
|
||||
boxSizing: "border-box",
|
||||
overflow: "hidden"
|
||||
};
|
||||
|
||||
export type pageList = {
|
||||
name: string,
|
||||
url: string
|
||||
}[];
|
||||
|
||||
export default function Home() {
|
||||
const [foundData, setFoundData] = useState<pageList | null>(null);
|
||||
|
||||
return (
|
||||
<CssVarsProvider defaultMode="dark" modeStorageKey="theme">
|
||||
<Sheet sx={mainSheetStyle}>
|
||||
<Typography level="h1" textAlign="center">
|
||||
Sherlock
|
||||
</Typography>
|
||||
|
||||
{foundData ?
|
||||
<Stack direction="column" spacing={2} sx={{height: "95%"}}>
|
||||
<Typography level="h4" textAlign="center">Found sites:</Typography>
|
||||
|
||||
<Grid container spacing={2} columns={{ xs: 4, sm: 6, md: 8, lg: 10, xl: 12 }} sx={{height: "95%", overflow: "auto"}}>
|
||||
{foundData.map(({name, url}) => {
|
||||
return (
|
||||
<Grid xs={2} key={"found-"+name}>
|
||||
<Link href={url} target="_blank" rel="noreferrer" textAlign="center" sx={{width: "100%", display: "block"}}>
|
||||
{name}
|
||||
</Link>
|
||||
</Grid>
|
||||
)
|
||||
})}
|
||||
</Grid>
|
||||
|
||||
<Button color="neutral" onClick={() => setFoundData(null)}>Back</Button>
|
||||
</Stack>
|
||||
: <Form setFoundData={setFoundData} /> }
|
||||
</Sheet>
|
||||
</CssVarsProvider>
|
||||
);
|
||||
}
|
@ -0,0 +1 @@
|
||||
../../sherlock/resources/data.json
|
@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
@ -0,0 +1,4 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
export default nextConfig;
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@fontsource/inter": "^5.0.16",
|
||||
"@mui/joy": "^5.0.0-beta.21",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"next": "14.1.2",
|
||||
"reset-css": "^5.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 629 B |
@ -0,0 +1,39 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
Loading…
Reference in new issue