enzostvs HF Staff commited on
Commit
6233641
·
1 Parent(s): 357c712

add admin rule

Browse files
app/api/collections/[id]/route.ts CHANGED
@@ -7,7 +7,13 @@ const prisma = new PrismaClient()
7
  export async function DELETE(request: Request, { params }: { params: { id: string } }) {
8
  const { headers } = request
9
 
10
- if (!headers?.get('Authorization') || headers.get('Authorization') !== `Bearer ${process.env.HF_TOKEN}`) {
 
 
 
 
 
 
11
  return Response.json(
12
  {
13
  status: 401,
 
7
  export async function DELETE(request: Request, { params }: { params: { id: string } }) {
8
  const { headers } = request
9
 
10
+ const staff_flag_id = headers.get('x-staff-flag-id') ?? undefined
11
+
12
+ // @ts-ignore
13
+ const HF_ADMIN = process?.env?.HF_ADMIN.split(',') ?? []
14
+ const is_admin = staff_flag_id ? HF_ADMIN.includes(staff_flag_id) : false
15
+
16
+ if (!is_admin) {
17
  return Response.json(
18
  {
19
  status: 401,
app/api/collections/[id]/visibility/route.ts ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaClient } from '@prisma/client'
2
+
3
+ const prisma = new PrismaClient()
4
+
5
+ export async function PUT(request: Request, { params }: { params: { id: string } }) {
6
+ const { headers } = request
7
+
8
+ const staff_flag_id = headers.get('x-staff-flag-id') ?? undefined
9
+
10
+ // @ts-ignore
11
+ const HF_ADMIN = process?.env?.HF_ADMIN.split(',') ?? []
12
+ const is_admin = staff_flag_id ? HF_ADMIN.includes(staff_flag_id) : false
13
+
14
+ if (!is_admin) {
15
+ return Response.json(
16
+ {
17
+ status: 401,
18
+ ok: false,
19
+ message: 'Wrong castle fam :/'
20
+ }
21
+ )
22
+ }
23
+
24
+ const collection = await prisma.collection.findUnique({
25
+ where: {
26
+ id: parseInt(params.id)
27
+ }
28
+ })
29
+ if (!collection) {
30
+ return Response.json(
31
+ {
32
+ status: 404,
33
+ ok: false
34
+ }
35
+ )
36
+ }
37
+
38
+ const new_image = await prisma.collection.update({
39
+ where: {
40
+ id: parseInt(params.id)
41
+ },
42
+ data: {
43
+ is_visible: true
44
+ }
45
+ })
46
+
47
+ return Response.json(
48
+ {
49
+ image: new_image,
50
+ message: `Image ${params.id} has been updated`,
51
+ status: 200,
52
+ ok: true
53
+ }
54
+ )
55
+ }
app/api/collections/route.ts CHANGED
@@ -3,10 +3,19 @@ import { PrismaClient } from '@prisma/client'
3
  const prisma = new PrismaClient()
4
 
5
  export async function GET(request: Request) {
 
6
  const { searchParams } = new URL(request.url)
7
  const userId = searchParams.get('id') ?? undefined
8
  const page = searchParams.get('page') ? parseInt(searchParams.get('page') as string) : 0
9
 
 
 
 
 
 
 
 
 
10
  const collections = await prisma.collection.findMany({
11
  orderBy: {
12
  id: 'desc'
@@ -14,6 +23,9 @@ export async function GET(request: Request) {
14
  where: {
15
  userId: {
16
  equals: userId
 
 
 
17
  }
18
  },
19
  take: 15,
 
3
  const prisma = new PrismaClient()
4
 
5
  export async function GET(request: Request) {
6
+ const { headers } = request
7
  const { searchParams } = new URL(request.url)
8
  const userId = searchParams.get('id') ?? undefined
9
  const page = searchParams.get('page') ? parseInt(searchParams.get('page') as string) : 0
10
 
11
+ const staff_flag_id = headers.get('x-staff-flag-id') ?? undefined
12
+
13
+ // @ts-ignore
14
+ const HF_ADMIN = process?.env?.HF_ADMIN.split(',') ?? []
15
+ const is_admin = staff_flag_id ? HF_ADMIN.includes(staff_flag_id) : false
16
+
17
+ console.log('is_admin', HF_ADMIN, is_admin, staff_flag_id)
18
+
19
  const collections = await prisma.collection.findMany({
20
  orderBy: {
21
  id: 'desc'
 
23
  where: {
24
  userId: {
25
  equals: userId
26
+ },
27
+ is_visible: {
28
+ equals: is_admin ? undefined : true
29
  }
30
  },
31
  take: 15,
app/api/login/route.ts CHANGED
@@ -2,7 +2,7 @@ export async function GET() {
2
  const REDIRECT_URI = `https://${process.env.SPACE_HOST}/login/callback`
3
  return Response.json(
4
  {
5
- redirect: `https://huggingface.co/oauth/authorize?client_id=${process.env.OAUTH_CLIENT_ID}&redirect_uri=${REDIRECT_URI}&scope=openid%20profile&state=STATE&response_type=code`,
6
  status: 200,
7
  ok: true
8
  }
 
2
  const REDIRECT_URI = `https://${process.env.SPACE_HOST}/login/callback`
3
  return Response.json(
4
  {
5
+ redirect: `https://huggingface.co/oauth/authorize?client_id=${process.env.OAUTH_CLIENT_ID}&redirect_uri=${REDIRECT_URI}&scope=openid%20profile%20email&state=STATE&response_type=code`,
6
  status: 200,
7
  ok: true
8
  }
app/api/me/route.ts CHANGED
@@ -13,10 +13,16 @@ export async function GET() {
13
  })
14
 
15
  const res = await request.clone().json().catch(() => ({}));
 
 
 
16
 
17
  return Response.json(
18
  {
19
- user: res,
 
 
 
20
  status: 200,
21
  ok: true
22
  }
 
13
  })
14
 
15
  const res = await request.clone().json().catch(() => ({}));
16
+ // @ts-ignore
17
+ const HF_ADMIN = process?.env?.HF_ADMIN.split(',') ?? []
18
+ const is_admin = res?.sub ? HF_ADMIN.includes(res?.sub) : false
19
 
20
  return Response.json(
21
  {
22
+ user: {
23
+ ...res,
24
+ is_admin,
25
+ },
26
  status: 200,
27
  ok: true
28
  }
app/api/route.ts CHANGED
@@ -41,7 +41,7 @@ export async function POST(
41
  const imageIsNSFW = await isImageNSFW(blob, global_headers)
42
  if (imageIsNSFW) return Response.json({ status: 401, ok: false, message: "Image is not safe for work." });
43
 
44
- const name = Date.now() + `-${inputs.replace(/[^a-zA-Z0-9]/g, '-').slice(0, 10).toLowerCase()}`
45
  const { ok, message } = await UploaderDataset(blob, name)
46
 
47
  if (!ok) return Response.json({ status: 500, ok: false, message });
 
41
  const imageIsNSFW = await isImageNSFW(blob, global_headers)
42
  if (imageIsNSFW) return Response.json({ status: 401, ok: false, message: "Image is not safe for work." });
43
 
44
+ const name = Date.now() + `-${inputs.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase()}`
45
  const { ok, message } = await UploaderDataset(blob, name)
46
 
47
  if (!ok) return Response.json({ status: 500, ok: false, message });
components/button/index.tsx CHANGED
@@ -1,15 +1,17 @@
1
  import classNames from "classnames";
 
2
 
3
  interface Props {
4
  children: React.ReactNode;
5
  disabled?: boolean;
6
- theme?: "primary" | "secondary" | "white";
7
- onClick?: () => void;
8
  }
9
  export const Button: React.FC<Props> = ({
10
  children,
11
  disabled,
12
  theme = "primary",
 
13
  ...props
14
  }) => {
15
  return (
@@ -19,11 +21,22 @@ export const Button: React.FC<Props> = ({
19
  {
20
  "bg-primary text-white border-primary": theme === "primary",
21
  "bg-white text-gray-900 border-white": theme === "white",
 
 
22
  "!bg-gray-400 !text-gray-600 !cursor-not-allowed !border-gray-400":
23
  disabled,
24
  }
25
  )}
26
  {...props}
 
 
 
 
 
 
 
 
 
27
  >
28
  {children}
29
  </button>
 
1
  import classNames from "classnames";
2
+ import { MouseEvent } from "react";
3
 
4
  interface Props {
5
  children: React.ReactNode;
6
  disabled?: boolean;
7
+ theme?: "primary" | "secondary" | "white" | "danger" | "success";
8
+ onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
9
  }
10
  export const Button: React.FC<Props> = ({
11
  children,
12
  disabled,
13
  theme = "primary",
14
+ onClick,
15
  ...props
16
  }) => {
17
  return (
 
21
  {
22
  "bg-primary text-white border-primary": theme === "primary",
23
  "bg-white text-gray-900 border-white": theme === "white",
24
+ "bg-red-500 text-white border-red-500": theme === "danger",
25
+ "bg-green-500 text-white border-green-500": theme === "success",
26
  "!bg-gray-400 !text-gray-600 !cursor-not-allowed !border-gray-400":
27
  disabled,
28
  }
29
  )}
30
  {...props}
31
+ onClick={
32
+ onClick
33
+ ? (e) => {
34
+ e.stopPropagation();
35
+ e.preventDefault();
36
+ onClick(e);
37
+ }
38
+ : undefined
39
+ }
40
  >
41
  {children}
42
  </button>
components/main/collections/collection.tsx CHANGED
@@ -1,9 +1,11 @@
1
  import { useMemo } from "react";
2
  import { motion } from "framer-motion";
 
 
3
 
4
- import { Collection as CollectionType, Image } from "@/utils/type";
5
  import { useInputGeneration } from "@/components/main/hooks/useInputGeneration";
6
- import { arrayBufferToBase64 } from "@/utils";
7
 
8
  interface Props {
9
  index: number;
@@ -63,8 +65,18 @@ export const Collection: React.FC<Props> = ({
63
  style={{
64
  backgroundImage: `url(https://huggingface.co/datasets/enzostvs/stable-diffusion-tpu-generations/resolve/main/images/${collection.file_name}.png)`,
65
  }}
66
- className="rounded-[33px] bg-gray-950 bg-cover absolute top-0 left-0 w-full h-full z-[-1] transition-all duration-200 group-hover:scale-110 bg-center"
 
 
 
 
 
67
  />
 
 
 
 
 
68
  </motion.div>
69
  </div>
70
  );
 
1
  import { useMemo } from "react";
2
  import { motion } from "framer-motion";
3
+ import classNames from "classnames";
4
+ import { AiFillEyeInvisible } from "react-icons/ai";
5
 
6
+ import { Image } from "@/utils/type";
7
  import { useInputGeneration } from "@/components/main/hooks/useInputGeneration";
8
+ import { Button } from "@/components/button";
9
 
10
  interface Props {
11
  index: number;
 
65
  style={{
66
  backgroundImage: `url(https://huggingface.co/datasets/enzostvs/stable-diffusion-tpu-generations/resolve/main/images/${collection.file_name}.png)`,
67
  }}
68
+ className={classNames(
69
+ "rounded-[33px] bg-gray-950 bg-cover absolute top-0 left-0 w-full h-full z-[-1] transition-all duration-200 group-hover:scale-110 bg-center",
70
+ {
71
+ "opacity-40": !collection.is_visible,
72
+ }
73
+ )}
74
  />
75
+ {!collection.is_visible && (
76
+ <div className="flex items-center justify-end px-5 py-6">
77
+ <AiFillEyeInvisible className="text-white text-2xl" />
78
+ </div>
79
+ )}
80
  </motion.div>
81
  </div>
82
  );
components/main/hooks/useCollections.ts CHANGED
@@ -1,14 +1,13 @@
1
  import { useState } from "react";
2
  import { useQuery, useQueryClient } from "@tanstack/react-query";
3
- import { useLocalStorage, useUpdateEffect } from "react-use";
4
  import _ from "lodash";
5
 
6
  import { useUser } from "@/utils/useUser";
7
 
8
  export const useCollections = (category: string) => {
9
  const [loading, setLoading] = useState(false);
10
- const { user } = useUser();
11
- const [myGenerationsId] = useLocalStorage<any>('my-own-generations', []);
12
 
13
  const client = useQueryClient();
14
 
@@ -24,6 +23,9 @@ export const useCollections = (category: string) => {
24
  queryParams.append('page', '0');
25
 
26
  const response = await fetch(`/api/collections?${queryParams.toString()}`, {
 
 
 
27
  method: "GET",
28
  })
29
 
@@ -38,6 +40,7 @@ export const useCollections = (category: string) => {
38
  };
39
  },
40
  {
 
41
  refetchOnMount: false,
42
  refetchOnWindowFocus: false,
43
  refetchOnReconnect: false,
@@ -51,6 +54,9 @@ export const useCollections = (category: string) => {
51
  queryParams.append('page', data?.pagination?.page,);
52
 
53
  const response = await fetch(`/api/collections?${queryParams.toString()}`, {
 
 
 
54
  method: "GET",
55
  })
56
 
 
1
  import { useState } from "react";
2
  import { useQuery, useQueryClient } from "@tanstack/react-query";
3
+ import { useUpdateEffect } from "react-use";
4
  import _ from "lodash";
5
 
6
  import { useUser } from "@/utils/useUser";
7
 
8
  export const useCollections = (category: string) => {
9
  const [loading, setLoading] = useState(false);
10
+ const { user, loading: userLoading } = useUser();
 
11
 
12
  const client = useQueryClient();
13
 
 
23
  queryParams.append('page', '0');
24
 
25
  const response = await fetch(`/api/collections?${queryParams.toString()}`, {
26
+ headers: {
27
+ 'x-staff-flag-id': user?.sub
28
+ },
29
  method: "GET",
30
  })
31
 
 
40
  };
41
  },
42
  {
43
+ enabled: !userLoading,
44
  refetchOnMount: false,
45
  refetchOnWindowFocus: false,
46
  refetchOnReconnect: false,
 
54
  queryParams.append('page', data?.pagination?.page,);
55
 
56
  const response = await fetch(`/api/collections?${queryParams.toString()}`, {
57
+ headers: {
58
+ 'x-staff-flag-id': user?.sub
59
+ },
60
  method: "GET",
61
  })
62
 
components/modal/modal.tsx CHANGED
@@ -1,9 +1,12 @@
1
  import { useMemo } from "react";
2
  import { motion } from "framer-motion";
3
  import Image from "next/image";
 
 
4
 
5
- import { arrayBufferToBase64 } from "@/utils";
6
  import { useCollection } from "./useCollection";
 
 
7
 
8
  interface Props {
9
  id: string;
@@ -30,16 +33,17 @@ const dropIn = {
30
  };
31
 
32
  export const Modal: React.FC<Props> = ({ id, onClose }) => {
33
- const { collection } = useCollection(id);
34
-
35
- if (!collection) return null;
36
 
37
  const formatDate = useMemo(() => {
38
  if (!collection) return;
39
- const date = new Date(collection.createdAt);
40
  return date.toLocaleDateString();
41
  }, [collection?.createdAt]);
42
 
 
 
43
  return (
44
  <motion.div
45
  onClick={onClose}
@@ -50,14 +54,28 @@ export const Modal: React.FC<Props> = ({ id, onClose }) => {
50
  >
51
  <motion.div
52
  onClick={(e) => e.stopPropagation()}
53
- className="max-w-2xl h-auto w-full z-[1] rounded-3xl overflow-hidden flex items-center justify-center flex-col gap-4 bg-white/30 backdrop-blur-sm px-2 pb-2 pt-2"
54
  variants={dropIn}
55
  initial="hidden"
56
  animate="visible"
57
  exit="exit"
58
  >
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  <Image
60
- src={`https://huggingface.co/datasets/enzostvs/stable-diffusion-tpu-generations/resolve/main/images/${collection.file_name}.png?raw=true`}
61
  alt="Generated image"
62
  className="object-center object-contain w-full h-full rounded-2xl"
63
  width={1024}
@@ -66,13 +84,13 @@ export const Modal: React.FC<Props> = ({ id, onClose }) => {
66
  <div
67
  className="bg-cover bg-center w-full h-full rounded-2xl bg-no-repeat z-[-1] absolute top-0 left-0 opacity-90 blur-xl"
68
  style={{
69
- backgroundImage: `url(https://huggingface.co/datasets/enzostvs/stable-diffusion-tpu-generations/resolve/main/images/${collection.file_name}.png?raw=true)`,
70
  }}
71
  />
72
  <div className="text-left w-full px-4 pb-3 pt-2">
73
  <p className="text-sm font-medium text-white/60 mb-1">{formatDate}</p>
74
  <p className="text-xl font-semibold text-white lowercase leading-snug">
75
- {collection.prompt}
76
  </p>
77
  </div>
78
  </motion.div>
 
1
  import { useMemo } from "react";
2
  import { motion } from "framer-motion";
3
  import Image from "next/image";
4
+ import { BsFillTrashFill } from "react-icons/bs";
5
+ import { AiFillCheckCircle } from "react-icons/ai";
6
 
 
7
  import { useCollection } from "./useCollection";
8
+ import { Button } from "../button";
9
+ import { useUser } from "@/utils/useUser";
10
 
11
  interface Props {
12
  id: string;
 
33
  };
34
 
35
  export const Modal: React.FC<Props> = ({ id, onClose }) => {
36
+ const { collection, updateVisibility, remove } = useCollection(id);
37
+ const { user } = useUser();
 
38
 
39
  const formatDate = useMemo(() => {
40
  if (!collection) return;
41
+ const date = new Date(collection?.createdAt);
42
  return date.toLocaleDateString();
43
  }, [collection?.createdAt]);
44
 
45
+ console.log(user);
46
+
47
  return (
48
  <motion.div
49
  onClick={onClose}
 
54
  >
55
  <motion.div
56
  onClick={(e) => e.stopPropagation()}
57
+ className="max-w-2xl h-auto w-full z-[1] rounded-3xl overflow-hidden relative flex items-center justify-center flex-col gap-4 bg-white/30 backdrop-blur-sm px-2 pb-2 pt-2"
58
  variants={dropIn}
59
  initial="hidden"
60
  animate="visible"
61
  exit="exit"
62
  >
63
+ {user?.is_admin && (
64
+ <div className="absolute p-2 rounded-full top-4 left-4 flex items-center justify-start gap-2 bg-black/20 backdrop-blur">
65
+ {!collection?.is_visible && (
66
+ <Button theme="white" onClick={updateVisibility}>
67
+ <AiFillCheckCircle />
68
+ Validate
69
+ </Button>
70
+ )}
71
+ <Button theme="danger" onClick={remove}>
72
+ <BsFillTrashFill />
73
+ Delete
74
+ </Button>
75
+ </div>
76
+ )}
77
  <Image
78
+ src={`https://huggingface.co/datasets/enzostvs/stable-diffusion-tpu-generations/resolve/main/images/${collection?.file_name}.png?raw=true`}
79
  alt="Generated image"
80
  className="object-center object-contain w-full h-full rounded-2xl"
81
  width={1024}
 
84
  <div
85
  className="bg-cover bg-center w-full h-full rounded-2xl bg-no-repeat z-[-1] absolute top-0 left-0 opacity-90 blur-xl"
86
  style={{
87
+ backgroundImage: `url(https://huggingface.co/datasets/enzostvs/stable-diffusion-tpu-generations/resolve/main/images/${collection?.file_name}.png?raw=true)`,
88
  }}
89
  />
90
  <div className="text-left w-full px-4 pb-3 pt-2">
91
  <p className="text-sm font-medium text-white/60 mb-1">{formatDate}</p>
92
  <p className="text-xl font-semibold text-white lowercase leading-snug">
93
+ {collection?.prompt}
94
  </p>
95
  </div>
96
  </motion.div>
components/modal/useCollection.ts CHANGED
@@ -1,9 +1,14 @@
1
  import { useMemo, useState } from "react"
2
  import { useQuery, useQueryClient } from "@tanstack/react-query"
3
 
4
- import { Collection } from "@/utils/type"
 
 
5
 
6
  export const useCollection = (id?: string) => {
 
 
 
7
  const { data: open } = useQuery(["modal"], () => {
8
  return null
9
  }, {
@@ -18,14 +23,67 @@ export const useCollection = (id?: string) => {
18
 
19
  const collection = useMemo(() => {
20
  const collections = client.getQueryData<Collection>(["collections"])
21
- if (!collections?.images) return null
22
-
 
 
 
23
  return collections?.images?.find((collection) => collection.id === id)
24
- }, [id])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  return {
27
  collection,
28
  open,
29
- setOpen
 
 
30
  }
31
  }
 
1
  import { useMemo, useState } from "react"
2
  import { useQuery, useQueryClient } from "@tanstack/react-query"
3
 
4
+ import { Collection, Image } from "@/utils/type"
5
+ import { useUser } from "@/utils/useUser"
6
+ import { set } from "lodash"
7
 
8
  export const useCollection = (id?: string) => {
9
+ const { user } = useUser()
10
+ const [loading, setLoading] = useState(false)
11
+
12
  const { data: open } = useQuery(["modal"], () => {
13
  return null
14
  }, {
 
23
 
24
  const collection = useMemo(() => {
25
  const collections = client.getQueryData<Collection>(["collections"])
26
+ if (!collections?.images) {
27
+ setOpen(null)
28
+ return null
29
+ }
30
+
31
  return collections?.images?.find((collection) => collection.id === id)
32
+ }, [id, loading])
33
+
34
+ const updateVisibility = async () => {
35
+ setLoading(true)
36
+ const response = await fetch(`/api/collections/${collection?.id}/visibility`, {
37
+ method: "PUT",
38
+ headers: {
39
+ 'x-staff-flag-id': user?.sub
40
+ }
41
+ })
42
+
43
+ const data = await response.json()
44
+ if (data.ok) {
45
+ client.setQueryData(["collections"], (old: any) => {
46
+ return {
47
+ ...old,
48
+ images: old.images.map((collection: Image) => {
49
+ if (collection.id === data.image.id) {
50
+ return data.image
51
+ }
52
+ return collection
53
+ })
54
+ }
55
+ })
56
+ }
57
+ setLoading(false)
58
+ }
59
+
60
+ const remove = async () => {
61
+ setLoading(true)
62
+ const response = await fetch(`/api/collections/${collection?.id}`, {
63
+ method: "DELETE",
64
+ headers: {
65
+ 'x-staff-flag-id': user?.sub
66
+ }
67
+ })
68
+
69
+ const data = await response.json()
70
+ if (data.ok) {
71
+ client.setQueryData(["collections"], (old: any) => {
72
+ return {
73
+ ...old,
74
+ images: old.images.filter((col: Image) => col.id !== collection?.id)
75
+ }
76
+ })
77
+ setOpen(null)
78
+ }
79
+ setLoading(false)
80
+ }
81
 
82
  return {
83
  collection,
84
  open,
85
+ setOpen,
86
+ updateVisibility,
87
+ remove
88
  }
89
  }
prisma/schema.prisma CHANGED
@@ -4,13 +4,14 @@ generator client {
4
 
5
  datasource db {
6
  provider = "sqlite"
7
- url = "file://data/dev.db"
8
  }
9
 
10
  model Collection {
11
- id Int @id @default(autoincrement())
12
- prompt String
13
- file_name String
14
- createdAt DateTime @default(now())
15
- userId String? @default("")
 
16
  }
 
4
 
5
  datasource db {
6
  provider = "sqlite"
7
+ url = "file:../data/dev.db"
8
  }
9
 
10
  model Collection {
11
+ id Int @id @default(autoincrement())
12
+ prompt String
13
+ file_name String
14
+ is_visible Boolean @default(false)
15
+ createdAt DateTime @default(now())
16
+ userId String? @default("")
17
  }
utils/type.ts CHANGED
@@ -12,6 +12,7 @@ export interface Image {
12
  file_name: string;
13
  prompt: string;
14
  createdAt: string;
 
15
  error?: string;
16
  loading?: boolean;
17
  }
 
12
  file_name: string;
13
  prompt: string;
14
  createdAt: string;
15
+ is_visible: boolean;
16
  error?: string;
17
  loading?: boolean;
18
  }