mirror of
https://github.com/fccview/cronmaster.git
synced 2025-12-23 22:18:20 -05:00
WIP auth and pwa integration
This commit is contained in:
26
README.md
26
README.md
@@ -49,7 +49,7 @@ If you find my projects helpful and want to fuel my late-night coding sessions w
|
||||
```bash
|
||||
services:
|
||||
cronjob-manager:
|
||||
image: ghcr.io/fccview/cronmaster:1.3.1
|
||||
image: ghcr.io/fccview/cronmaster:1.4.0
|
||||
container_name: cronmaster
|
||||
user: "root"
|
||||
ports:
|
||||
@@ -58,17 +58,26 @@ services:
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DOCKER=true
|
||||
# Legacy used to be NEXT_PUBLIC_HOST_PROJECT_DIR, this was causing issues on runtime.
|
||||
|
||||
# --- MAP HOST PROJECT DIRECTORY, THIS IS MANDATORY FOR SCRIPTS TO WORK
|
||||
- HOST_PROJECT_DIR=/path/to/cronmaster/directory
|
||||
- NEXT_PUBLIC_CLOCK_UPDATE_INTERVAL=30000
|
||||
# If docker struggles to find your crontab user, update this variable with it.
|
||||
# Obviously replace fccview with your user - find it with: ls -asl /var/spool/cron/crontabs/
|
||||
|
||||
# --- PASSWORD PROTECTION
|
||||
# Uncomment to enable password protection (replace "password" with your own)
|
||||
#- AUTH_PASSWORD=password
|
||||
|
||||
# --- CRONTAB USERS
|
||||
# This is used to read the crontabs for the specific user.
|
||||
# replace fccview with your user - find it with: ls -asl /var/spool/cron/crontabs/
|
||||
# For multiple users, use comma-separated values: HOST_CRONTAB_USER=fccview,root,user1,user2
|
||||
# - HOST_CRONTAB_USER=fccview
|
||||
volumes:
|
||||
# --- MOUNT DOCKER SOCKET
|
||||
# Mount Docker socket to execute commands on host
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
# --- MOUNT DATA
|
||||
# These are needed if you want to keep your data on the host machine and not wihin the docker volume.
|
||||
# DO NOT change the location of ./scripts as all cronjobs that use custom scripts created via the app
|
||||
# will target this folder (thanks to the HOST_PROJECT_DIR variable set above)
|
||||
@@ -76,14 +85,14 @@ services:
|
||||
- ./data:/app/data
|
||||
- ./snippets:/app/snippets
|
||||
|
||||
# Use host PID namespace for host command execution
|
||||
# Run in privileged mode for nsenter access
|
||||
# --- USE HOST PID NAMESPACE FOR HOST COMMAND EXECUTION
|
||||
# --- RUN IN PRIVILEGED MODE FOR NSENTER ACCESS
|
||||
pid: "host"
|
||||
privileged: true
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
|
||||
# Default platform is set to amd64, uncomment to use arm64.
|
||||
# --- DEFAULT PLATFORM IS SET TO AMD64, UNCOMMENT TO USE ARM64.
|
||||
#platform: linux/arm64
|
||||
```
|
||||
|
||||
@@ -244,6 +253,9 @@ I would like to thank the following members for raising issues and help test/deb
|
||||
<td align="center" valign="top" width="20%">
|
||||
<a href="https://github.com/ActxLeToucan"><img width="100" height="100" src="https://avatars.githubusercontent.com/u/56509120?u=b0a684dfa1fcf8f3f41c2ead37f6441716d8bd62&v=4&size=100"><br />ActxLeToucan</a>
|
||||
</td>
|
||||
<td align="center" valign="top" width="20%">
|
||||
<a href="https://github.com/mrtimothyduong"><img width="100" height="100" src="https://avatars.githubusercontent.com/u/34667840?u=b54354da56681c17ca58366a68a6a94c80f77a1d&v=4&size=100"><br />mrtimothyduong</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -159,14 +159,8 @@ export const CronJobList = ({ cronJobs, scripts }: CronJobListProps) => {
|
||||
const result = await runCronJob(id);
|
||||
if (result.success) {
|
||||
showToast("success", "Cron job executed successfully");
|
||||
if (result.output) {
|
||||
console.log("Command output:", result.output);
|
||||
}
|
||||
} else {
|
||||
showToast("error", "Failed to execute cron job", result.message);
|
||||
if (result.output) {
|
||||
console.error("Command error:", result.output);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(
|
||||
|
||||
99
app/_components/features/LoginForm/LoginForm.tsx
Normal file
99
app/_components/features/LoginForm/LoginForm.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "../../ui/Button";
|
||||
import { Input } from "../../ui/Input";
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "../../ui/Card";
|
||||
import { Lock, Eye, EyeOff } from "lucide-react";
|
||||
|
||||
|
||||
export const LoginForm = () => {
|
||||
const [password, setPassword] = useState("");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ password }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
router.push("/");
|
||||
} else {
|
||||
setError(result.message || "Login failed");
|
||||
}
|
||||
} catch (error) {
|
||||
setError("An error occurred. Please try again.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-md shadow-xl">
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mb-4">
|
||||
<Lock className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<CardTitle>Welcome to Cr*nMaster</CardTitle>
|
||||
<CardDescription>Enter your password to continue</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Enter password"
|
||||
className="pr-10"
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff className="w-4 h-4" />
|
||||
) : (
|
||||
<Eye className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-sm text-red-500 bg-red-500/10 border border-red-500/20 rounded-md p-3">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isLoading || !password.trim()}
|
||||
>
|
||||
{isLoading ? "Signing in..." : "Sign In"}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
43
app/_components/ui/LogoutButton.tsx
Normal file
43
app/_components/ui/LogoutButton.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "./Button";
|
||||
import { LogOut } from "lucide-react";
|
||||
|
||||
|
||||
export const LogoutButton = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const handleLogout = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch("/api/auth/logout", {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
router.push("/login");
|
||||
router.refresh();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Logout error:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleLogout}
|
||||
disabled={isLoading}
|
||||
title="Logout"
|
||||
>
|
||||
<LogOut className="h-[1.2rem] w-[1.2rem]" />
|
||||
<span className="sr-only">Logout</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
44
app/api/auth/login/route.ts
Normal file
44
app/api/auth/login/route.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { password } = await request.json()
|
||||
|
||||
const authPassword = process.env.AUTH_PASSWORD
|
||||
|
||||
if (!authPassword) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Authentication not configured' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (password !== authPassword) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Invalid password' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const response = NextResponse.json(
|
||||
{ success: true, message: 'Login successful' },
|
||||
{ status: 200 }
|
||||
)
|
||||
|
||||
response.cookies.set('cronmaster-auth', 'authenticated', {
|
||||
httpOnly: true,
|
||||
secure: request.url.startsWith('https://'),
|
||||
sameSite: 'lax',
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
path: '/',
|
||||
})
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('Login error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
26
app/api/auth/logout/route.ts
Normal file
26
app/api/auth/logout/route.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const response = NextResponse.json(
|
||||
{ success: true, message: 'Logout successful' },
|
||||
{ status: 200 }
|
||||
)
|
||||
|
||||
response.cookies.set('cronmaster-auth', '', {
|
||||
httpOnly: true,
|
||||
secure: request.url.startsWith('https://'),
|
||||
sameSite: 'lax',
|
||||
maxAge: 0,
|
||||
path: '/',
|
||||
})
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ export async function GET(request: NextRequest) {
|
||||
statusDetails = "Moderate resource usage - monitoring recommended";
|
||||
}
|
||||
|
||||
let mainInterface = null;
|
||||
let mainInterface: any = null;
|
||||
if (Array.isArray(networkInfo) && networkInfo.length > 0) {
|
||||
mainInterface = networkInfo.find(net =>
|
||||
net.iface && !net.iface.includes('lo') && net.operstate === 'up'
|
||||
|
||||
@@ -17,8 +17,18 @@ const inter = Inter({
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Cr*nMaster - Cron Management made easy",
|
||||
description:
|
||||
"The ultimate cron job management platform with intelligent scheduling, real-time monitoring, and powerful automation tools",
|
||||
description: "The ultimate cron job management platform with intelligent scheduling, real-time monitoring, and powerful automation tools",
|
||||
manifest: "/manifest.json",
|
||||
themeColor: "#3b82f6",
|
||||
viewport: "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no",
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
statusBarStyle: "default",
|
||||
title: "Cr*nMaster",
|
||||
},
|
||||
formatDetection: {
|
||||
telephone: false,
|
||||
},
|
||||
icons: {
|
||||
icon: "/logo.png",
|
||||
shortcut: "/logo.png",
|
||||
@@ -33,10 +43,18 @@ export default function RootLayout({
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<meta name="application-name" content="Cr*nMaster" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="apple-mobile-web-app-title" content="Cr*nMaster" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<link rel="apple-touch-icon" href="/icon-192x192.png" />
|
||||
</head>
|
||||
<body className={`${inter.variable} ${jetbrainsMono.variable} font-sans`}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
defaultTheme="dark"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
|
||||
14
app/login/page.tsx
Normal file
14
app/login/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
'use server';
|
||||
|
||||
import { LoginForm } from "../_components/features/LoginForm/LoginForm";
|
||||
|
||||
export default async function LoginPage() {
|
||||
return (
|
||||
<div className="min-h-screen relative">
|
||||
<div className="hero-gradient absolute inset-0 -z-10"></div>
|
||||
<div className="relative z-10 flex items-center justify-center min-h-screen p-4">
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
app/page.tsx
10
app/page.tsx
@@ -3,6 +3,7 @@ import { TabbedInterface } from "./_components/TabbedInterface";
|
||||
import { getCronJobs } from "./_utils/system";
|
||||
import { fetchScripts } from "./_server/actions/scripts";
|
||||
import { ThemeToggle } from "./_components/ui/ThemeToggle";
|
||||
import { LogoutButton } from "./_components/ui/LogoutButton";
|
||||
import { ToastContainer } from "./_components/ui/Toast";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -52,7 +53,7 @@ export default async function Home() {
|
||||
<div className="relative z-10">
|
||||
<header className="border-b border-border/50 bg-background/80 backdrop-blur-md sticky top-0 z-20 shadow-sm lg:h-[90px]">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="flex items-center justify-between lg:justify-center">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative">
|
||||
<img src="/logo.png" alt="logo" className="w-14 h-14" />
|
||||
@@ -67,6 +68,11 @@ export default async function Home() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{process.env.AUTH_PASSWORD && (
|
||||
<div className="lg:absolute lg:right-10">
|
||||
<LogoutButton />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -82,7 +88,7 @@ export default async function Home() {
|
||||
|
||||
<ToastContainer />
|
||||
|
||||
<div className="flex items-center gap-2 fixed bottom-4 left-4 lg:right-4 lg:left-auto z-10 bg-background rounded-lg">
|
||||
<div className="flex items-center gap-2 fixed bottom-4 left-4 lg:right-4 lg:left-auto z-10 bg-background/80 backdrop-blur-md border border-border/50 rounded-lg p-1">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
cronjob-manager:
|
||||
image: ghcr.io/fccview/cronmaster:1.3.1
|
||||
image: ghcr.io/fccview/cronmaster:1.4.0
|
||||
container_name: cronmaster
|
||||
user: "root"
|
||||
ports:
|
||||
@@ -9,17 +9,26 @@ services:
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DOCKER=true
|
||||
# Legacy used to be NEXT_PUBLIC_HOST_PROJECT_DIR, this was causing issues on runtime.
|
||||
|
||||
# --- MAP HOST PROJECT DIRECTORY, THIS IS MANDATORY FOR SCRIPTS TO WORK
|
||||
- HOST_PROJECT_DIR=/path/to/cronmaster/directory
|
||||
- NEXT_PUBLIC_CLOCK_UPDATE_INTERVAL=30000
|
||||
# If docker struggles to find your crontab user, update this variable with it.
|
||||
# Obviously replace fccview with your user - find it with: ls -asl /var/spool/cron/crontabs/
|
||||
|
||||
# --- PASSWORD PROTECTION
|
||||
# Uncomment to enable password protection (replace "password" with your own)
|
||||
#- AUTH_PASSWORD=password
|
||||
|
||||
# --- CRONTAB USERS
|
||||
# This is used to read the crontabs for the specific user.
|
||||
# replace fccview with your user - find it with: ls -asl /var/spool/cron/crontabs/
|
||||
# For multiple users, use comma-separated values: HOST_CRONTAB_USER=fccview,root,user1,user2
|
||||
# - HOST_CRONTAB_USER=fccview
|
||||
volumes:
|
||||
# --- MOUNT DOCKER SOCKET
|
||||
# Mount Docker socket to execute commands on host
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
# --- MOUNT DATA
|
||||
# These are needed if you want to keep your data on the host machine and not wihin the docker volume.
|
||||
# DO NOT change the location of ./scripts as all cronjobs that use custom scripts created via the app
|
||||
# will target this folder (thanks to the HOST_PROJECT_DIR variable set above)
|
||||
@@ -27,12 +36,12 @@ services:
|
||||
- ./data:/app/data
|
||||
- ./snippets:/app/snippets
|
||||
|
||||
# Use host PID namespace for host command execution
|
||||
# Run in privileged mode for nsenter access
|
||||
# --- USE HOST PID NAMESPACE FOR HOST COMMAND EXECUTION
|
||||
# --- RUN IN PRIVILEGED MODE FOR NSENTER ACCESS
|
||||
pid: "host"
|
||||
privileged: true
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
|
||||
# Default platform is set to amd64, uncomment to use arm64.
|
||||
# --- DEFAULT PLATFORM IS SET TO AMD64, UNCOMMENT TO USE ARM64.
|
||||
#platform: linux/arm64
|
||||
|
||||
37
middleware.ts
Normal file
37
middleware.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl
|
||||
|
||||
if (pathname.startsWith('/api/') || pathname.startsWith('/_next/') || pathname.includes('.')) {
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
const authPassword = process.env.AUTH_PASSWORD
|
||||
|
||||
if (!authPassword) {
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
const isAuthenticated = request.cookies.has('cronmaster-auth')
|
||||
|
||||
if (pathname === '/login') {
|
||||
if (isAuthenticated || !authPassword) {
|
||||
return NextResponse.redirect(new URL('/', request.url))
|
||||
}
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return NextResponse.redirect(new URL('/login', request.url))
|
||||
}
|
||||
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/((?!api|_next/static|_next/image|favicon.ico).*)',
|
||||
],
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
const withPWA = require('next-pwa')({
|
||||
dest: 'public',
|
||||
register: true,
|
||||
skipWaiting: true,
|
||||
disable: process.env.NODE_ENV === 'development'
|
||||
})
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
module.exports = withPWA(nextConfig)
|
||||
|
||||
@@ -23,12 +23,15 @@
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"autoprefixer": "^10.0.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"clsx": "^2.0.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"cron-parser": "^5.3.0",
|
||||
"cronstrue": "^3.2.0",
|
||||
"lucide-react": "^0.294.0",
|
||||
"minimatch": "^10.0.3",
|
||||
"next": "14.0.4",
|
||||
"next-pwa": "^5.6.0",
|
||||
"next-themes": "^0.2.1",
|
||||
"postcss": "^8",
|
||||
"react": "^18",
|
||||
@@ -40,6 +43,8 @@
|
||||
"typescript": "^5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/minimatch": "^6.0.0",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.0.4"
|
||||
}
|
||||
|
||||
26
public/manifest.json
Normal file
26
public/manifest.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "Cr*nMaster",
|
||||
"short_name": "Cr*nMaster",
|
||||
"description": "The ultimate cron job management platform with intelligent scheduling, real-time monitoring, and powerful automation tools",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#0f0f23",
|
||||
"theme_color": "#3b82f6",
|
||||
"orientation": "portrait-primary",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
}
|
||||
],
|
||||
"categories": ["productivity", "utilities"],
|
||||
"lang": "en"
|
||||
}
|
||||
1
public/sw.js
Normal file
1
public/sw.js
Normal file
File diff suppressed because one or more lines are too long
1
public/workbox-1bb06f5e.js
Normal file
1
public/workbox-1bb06f5e.js
Normal file
File diff suppressed because one or more lines are too long
@@ -1,6 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "es6"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"es6"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -19,9 +23,18 @@
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user