Browse Source

Refactoring

master
Archie 1 month ago
parent
commit
7e80a6873e
27 changed files with 122 additions and 401 deletions
  1. +2
    -0
      .prettierignore
  2. +2
    -86
      README.md
  3. +0
    -15
      next-env.d.ts
  4. +0
    -5
      next.config.js
  5. +1
    -2
      package.json
  6. +0
    -1
      src/components/index.ts
  7. +3
    -14
      src/components/page.js
  8. +0
    -8
      src/lib/getAddress.ts
  9. +0
    -3
      src/lib/index.ts
  10. +0
    -86
      src/lib/theme.ts
  11. +0
    -2
      src/logos/index.tsx
  12. +0
    -0
      src/logos/pulsar.js
  13. +0
    -0
      src/logos/spectare.js
  14. +10
    -14
      src/pages/_app.js
  15. +3
    -11
      src/pages/_document.js
  16. +71
    -0
      src/pages/api/login.js
  17. +0
    -56
      src/pages/api/login.ts
  18. +4
    -6
      src/pages/api/me.js
  19. +6
    -23
      src/pages/index.js
  20. +13
    -16
      src/pages/login.js
  21. +0
    -2
      src/types/index.ts
  22. +0
    -5
      src/types/session.ts
  23. +0
    -20
      src/types/user.ts
  24. +6
    -0
      src/utils/getAddress.js
  25. +1
    -2
      src/utils/user.js
  26. +0
    -19
      tsconfig.json
  27. +0
    -5
      yarn.lock

+ 2
- 0
.prettierignore View File

@ -0,0 +1,2 @@
.next
node_modules

+ 2
- 86
README.md View File

@ -1,87 +1,3 @@
<p align="center">
<img src="https://i.imgur.com/SGWCbtl.png" height="120" />
</p>
# Hub
The central component of The Alles Platform. This application is responsible for handing user authentication, third-party app authorization and user account settings.
![Screenshot](https://i.imgur.com/uata61x.png)
## Running locally
Install the dependencies:
```
yarn
```
Once that's done, you can run this command to start the development server:
```
yarn dev
```
Now you can start developing! 🎉
## Running in production
Requirements:
- Docker
- A functioning instance of [Nexus](https://github.com/alleshq/nexus)
- A valid pair of Nexus credentials
To get started, rename the `docker-compose-example.yml` file to `docker-compose.yml` and edit it to fit your needs.
Make sure that you have all the necessary environment variables configured:
```
NEXUS_ID=
NEXUS_SECRET=
NEXUS_URI=
PUBLIC_URI=
NEXT_PUBLIC_COOKIE_DOMAIN=
```
> Note: Sometimes a leading do is required in front of `COOKIE_DOMAIN` if you want it to be available across subdomains.
Then start the application by running:
```
docker-compose up
```
Now you have your own production instance of The Alles Hub! 🎉
## Contributing
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
2. After you have made changes, [open a pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request).
Here is a [list of issues](https://github.com/alleshq/hub/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+for+beginners%22) that are good for beginners.
## Maintainers
<table>
<tr>
<td align="center">
<a href="https://github.com/danteissaias">
<img
src="https://avatars3.githubusercontent.com/u/13090065?s=460&v=4"
width="100px;"
alt=""
/>
<br />
<sub> <b>Dante Issaias</b></sub></a
>
</td>
<td align="center">
<a href="https://github.com/archiebaer"
><img
src="https://avatars2.githubusercontent.com/u/42045366?s=460&u=19e4ba3c1703180c41a874131a55b505f9fb059f&v=4"
width="100px;"
alt=""
/><br /><sub><b>Archie Baer</b></sub></a
>
</td>
</tr>
</table>
This is the account hub for Alles

+ 0
- 15
next-env.d.ts View File

@ -1,15 +0,0 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
// Extend the NodeJS namespace with Next.js-defined properties
declare namespace NodeJS {
interface ProcessEnv {
readonly NODE_ENV: "development" | "production" | "test";
readonly NEXUS_ID: string;
readonly NEXUS_SECRET: string;
readonly NEXUS_URI: string;
readonly PUBLIC_URI: string;
readonly NEXT_PUBLIC_COOKIE_DOMAIN: string;
readonly buildTimestamp: string;
}
}

+ 0
- 5
next.config.js View File

@ -1,5 +0,0 @@
module.exports = {
env: {
buildTimestamp: new Date(),
},
};

+ 1
- 2
package.json View File

@ -3,14 +3,13 @@
"start": "next start",
"dev": "next dev",
"build": "NODE_ENV=production next build",
"format": "prettier --write \"src/**/*.+(ts|tsx)\" \"*.+(json|ts|md|js)\""
"format": "prettier --write \"./**/*.+(js)\""
},
"dependencies": {
"@alleshq/reactants": "^1.2.0",
"axios": "^0.20.0",
"classnames": "^2.2.6",
"es-cookie": "^1.3.2",
"moment": "^2.27.0",
"next": "^9.5.2",
"next-cookies": "^2.0.3",
"react": "^16.13.1",

+ 0
- 1
src/components/index.ts View File

@ -1 +0,0 @@
export * from "./page";

src/components/page.tsx → src/components/page.js View File

@ -6,22 +6,16 @@ import {
Menu,
Header,
Button,
useTheme,
} from "@alleshq/reactants";
import { Circle } from "react-feather";
import Link from "next/link";
import Router from "next/router";
import Head from "next/head";
import moment from "moment";
import { remove as removeCookie } from "es-cookie";
import { useUser, useTheme } from "../lib";
import { useUser } from "../utils/user";
type Props = {
authenticated?: boolean;
title?: string;
breadcrumbs?: React.ReactNode;
};
export const Page: React.FC<Props> = ({
export const Page = ({
children,
authenticated = true,
title,
@ -29,7 +23,6 @@ export const Page: React.FC = ({
}) => {
const user = useUser();
const { toggleTheme } = useTheme();
const d = process.env.buildTimestamp;
const logOut = () => {
const isProduction = process.env.NODE_ENV === "production";
@ -119,10 +112,6 @@ export const Page: React.FC = ({
<footer className="border-gray-400 flex items-center justify-center text-sm absolute bottom-0 w-full h-15 text-gray-500 dark:text-gray-400">
<div className="w-full max-w-2xl px-5">
<div className="float-left space-x-5">
Built on {moment(d).format("LL")} at {moment(d).format("LT")}
</div>
<div className="float-right space-x-7">
<a
href="https://github.com/alleshq/hub"

+ 0
- 8
src/lib/getAddress.ts View File

@ -1,8 +0,0 @@
import { NextApiRequest } from "next";
export const getAddress = ({ headers, connection }: NextApiRequest) => {
if (headers["x-forwarded-for"]) {
const ips = (headers["x-forwarded-for"] as string).split(", ");
return ips[ips.length - 1];
} else return connection.remoteAddress;
};

+ 0
- 3
src/lib/index.ts View File

@ -1,3 +0,0 @@
export * from "./user";
export * from "./theme";
export * from "./getAddress";

+ 0
- 86
src/lib/theme.ts View File

@ -1,86 +0,0 @@
// https://github.com/pacocoursey/paco/blob/master/lib/theme.ts
import { useCallback, useEffect } from "react";
import useSWR from "swr";
import * as cookies from "es-cookie";
export type Theme = "dark" | "light";
export const themeCookieName = "theme";
const isServer = typeof window === "undefined";
const getTheme = (): Theme => {
if (isServer) return "light";
return (cookies.get(themeCookieName) as Theme) || "light";
};
const setDarkMode = () => {
try {
cookies.set(themeCookieName, "dark");
document.documentElement.classList.add("dark");
} catch (err) {
console.error(err);
}
};
const setLightMode = () => {
try {
cookies.set(themeCookieName, "light");
document.documentElement.classList.remove("dark");
} catch (err) {
console.error(err);
}
};
const disableAnimation = () => {
const css = document.createElement("style");
css.type = "text/css";
css.appendChild(
document.createTextNode(
`* {
-webkit-transition: none !important;
-moz-transition: none !important;
-o-transition: none !important;
-ms-transition: none !important;
transition: none !important;
}`
)
);
document.head.appendChild(css);
return () => {
// Force restyle
(() => window.getComputedStyle(css).opacity)();
document.head.removeChild(css);
};
};
export const useTheme = () => {
const { data: theme, mutate } = useSWR(themeCookieName, getTheme, {
initialData: getTheme(),
});
const setTheme = useCallback(
(newTheme: Theme) => {
mutate(newTheme, false);
},
[mutate]
);
useEffect(() => {
const enable = disableAnimation();
if (theme === "dark") {
setDarkMode();
} else {
setLightMode();
}
enable();
}, [theme]);
return {
theme,
setTheme,
toggleTheme: () => setTheme(!theme || theme === "dark" ? "light" : "dark"),
};
};

+ 0
- 2
src/logos/index.tsx View File

@ -1,2 +0,0 @@
export * from "./spectare";
export * from "./pulsar";

src/logos/pulsar.tsx → src/logos/pulsar.js View File


src/logos/spectare.tsx → src/logos/spectare.js View File


src/pages/_app.tsx → src/pages/_app.js View File

@ -2,15 +2,9 @@ import "@alleshq/reactants/dist/index.css";
import axios from "axios";
import App from "next/app";
import Router from "next/router";
import type { AppProps, AppContext } from "next/app";
import type { User } from "../types";
import { UserContext } from "../lib";
import { UserContext } from "../utils/user";
type Props = {
user: User;
} & AppProps;
export default function Hub({ Component, pageProps, user }: Props) {
export default function Hub({ Component, pageProps, user }) {
return (
<UserContext.Provider value={user}>
<Component {...pageProps} />
@ -18,13 +12,12 @@ export default function Hub({ Component, pageProps, user }: Props) {
);
}
Hub.getInitialProps = async (appContext: AppContext) => {
Hub.getInitialProps = async (appContext) => {
const props = await App.getInitialProps(appContext);
const { ctx } = appContext;
const isServer = typeof window === "undefined";
const redirect = (location: string) =>
const redirect = (location) =>
isServer
? ctx.res.writeHead(302, { location }).end()
: /^https?:\/\/|^\/\//i.test(location)
@ -43,7 +36,7 @@ Hub.getInitialProps = async (appContext: AppContext) => {
try {
const cookie = ctx.req?.headers.cookie ?? "";
const headers = isServer ? { cookie } : {};
const user: User = await axios
const user = await axios
.get(`${process.env.PUBLIC_URI ?? ""}/api/me`, { headers })
.then((res) => res.data);
@ -52,9 +45,12 @@ Hub.getInitialProps = async (appContext: AppContext) => {
redirect(ctx.query.next?.toString() ?? "/");
return { ...props, user };
} catch (error) {
} catch (err) {
// At this point we're 100% sure the token is invalid.
if (!redirectIfLoggedInPaths.includes(ctx.pathname))
if (
!redirectIfLoggedInPaths.includes(ctx.pathname) &&
!allowGuestPaths.includes(ctx.pathname)
)
redirect(`/login?next=${ctx.pathname}`);
return { ...props };

src/pages/_document.tsx → src/pages/_document.js View File

@ -1,20 +1,12 @@
import NextDocument, {
Html,
Head,
Main,
NextScript,
DocumentContext,
DocumentInitialProps,
} from "next/document";
import NextDocument, { Html, Head, Main, NextScript } from "next/document";
import nextCookies from "next-cookies";
import classnames from "classnames";
import { Theme } from "../lib";
export default class Document extends NextDocument<{ theme: Theme }> {
export default class Document extends NextDocument {
static async getInitialProps(ctx) {
const initialProps = await NextDocument.getInitialProps(ctx);
const cookies = nextCookies(ctx);
return { ...initialProps, theme: (cookies.theme as Theme) ?? "light" };
return { ...initialProps, theme: cookies.theme ?? "light" };
}
render() {

+ 71
- 0
src/pages/api/login.js View File

@ -0,0 +1,71 @@
import axios from "axios";
import { getAddress } from "../../utils/getAddress";
export default async (req, res) => {
if (
!req.body ||
typeof req.body.name !== "string" ||
typeof req.body.tag !== "string" ||
typeof req.body.password !== "string"
) {
return res.status(400).send({ err: "badRequest" });
}
// Fake delay
const delay = Math.floor(Math.random() * 200) + 50;
await new Promise((resolve) => setTimeout(() => resolve(), delay));
try {
// Get user id from nametag
const { id } = await axios
.get(
`${process.env.NEXUS_URI}/nametag?name=${encodeURIComponent(
req.body.name
)}&tag=${encodeURIComponent(req.body.tag)}`,
{
auth: {
username: process.env.NEXUS_ID,
password: process.env.NEXUS_SECRET,
},
}
)
.then((res) => res.data);
// Validate password
const { matches } = await axios
.post(
`${process.env.NEXUS_URI}/users/${id}/password/verify`,
{ password: req.body.password },
{
auth: {
username: process.env.NEXUS_ID,
password: process.env.NEXUS_SECRET,
},
}
)
.then((res) => res.data);
if (!matches) throw Error();
// Create session
const { token } = await axios
.post(
`${process.env.NEXUS_URI}/sessions`,
{
user: id,
address: getAddress(req),
},
{
auth: {
username: process.env.NEXUS_ID,
password: process.env.NEXUS_SECRET,
},
}
)
.then((res) => res.data);
res.json({ token });
} catch (err) {
return res.status(400).json({ err: "user.signIn.credentials" });
}
};

+ 0
- 56
src/pages/api/login.ts View File

@ -1,56 +0,0 @@
import axios from "axios";
import type { NextApiRequest, NextApiResponse } from "next";
import type { Session } from "../../types";
import { getAddress } from "../../lib";
export default async (req: NextApiRequest, res: NextApiResponse) => {
if (
!req.body ||
typeof req.body.name !== "string" ||
typeof req.body.tag !== "string" ||
typeof req.body.password !== "string"
) {
return res.status(400).send({ err: "badRequest" });
}
const delay = Math.floor(Math.random() * 200) + 50;
await new Promise((resolve) => setTimeout(() => resolve(), delay));
const name = encodeURIComponent(req.body.name);
const tag = encodeURIComponent(req.body.tag);
const password = req.body.password;
const { NEXUS_ID, NEXUS_SECRET, NEXUS_URI } = process.env;
const auth = { username: NEXUS_ID, password: NEXUS_SECRET };
try {
// Get user id from nametag
const {
id,
}: Omit<
Session,
"user"
> = await axios
.get(`${NEXUS_URI}/nametag?name=${name}&tag=${tag}`, { auth })
.then((res) => res.data);
// Validate password
const { matches } = await axios
.post(`${NEXUS_URI}/users/${id}/password/verify`, { password }, { auth })
.then((res) => res.data);
if (!matches) throw Error();
// Create session
const user = id;
const address = getAddress(req);
const { token } = await axios
.post(`${NEXUS_URI}/sessions`, { user, address }, { auth })
.then((res) => res.data);
res.json({ token });
} catch (error) {
return res.status(400).json({ err: "user.signIn.credentials" });
}
};

src/pages/api/me.ts → src/pages/api/me.js View File

@ -1,8 +1,6 @@
import axios from "axios";
import type { NextApiRequest, NextApiResponse } from "next";
import type { Session, User } from "../../types";
export default async (req: NextApiRequest, res: NextApiResponse) => {
export default async (req, res) => {
if (!req.cookies.sessionToken) {
return res.status(401).send({ err: "badAuthorization" });
}
@ -13,12 +11,12 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
try {
// Get session from token
const session: Omit<Session, "token"> = await axios
const session = await axios
.post(`${NEXUS_URI}/sessions/token`, { token }, { auth })
.then((res) => res.data);
// Get user by id
const user: User = await axios
const user = await axios
.get(`${NEXUS_URI}/users/${session.user}`, { auth })
.then((res) => res.data);
@ -31,7 +29,7 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
plus: user.plus,
createdAt: user.createdAt,
});
} catch (error) {
} catch (err) {
res.status(500).send({ err: "internalError" });
}
};

src/pages/index.tsx → src/pages/index.js View File

@ -2,34 +2,17 @@ import { Box } from "@alleshq/reactants";
import {
User as UserIcon,
Shield,
Icon,
Grid,
Award,
PlusCircle,
PlusSquare,
} from "react-feather";
import Link from "next/link";
import { Page } from "../components";
import { useUser } from "../lib";
import { Spectare, Pulsar } from "../logos";
import { Page } from "../components/page";
import { useUser } from "../utils/user";
import { Spectare } from "../logos/spectare";
import { Pulsar } from "../logos/pulsar";
interface Category {
name: string;
icon: Icon;
links: {
text: string;
href: string;
external?: boolean;
}[];
}
interface Product {
name: string;
logo: React.ReactNode;
url: string;
}
const categories: Category[] = [
const categories = [
{
name: "Profile and Personalisation",
icon: UserIcon,
@ -118,7 +101,7 @@ const TextLogo = ({ children }) => (
</div>
);
const products: Product[] = [
const products = [
{
name: "Micro",
logo: <TextLogo>μ</TextLogo>,

src/pages/login.tsx → src/pages/login.js View File

@ -8,20 +8,19 @@ import {
Transition,
} from "@alleshq/reactants";
import { LogIn, Circle } from "react-feather";
import { useState, FormEvent } from "react";
import { useState } from "react";
import Router from "next/router";
import { ParsedUrlQuery } from "querystring";
import { set as setCookie } from "es-cookie";
import { Page } from "../components";
import { Page } from "../components/page";
export default function Login({ query }: { query: ParsedUrlQuery }) {
const [nametag, setNametag] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [showError, setShowError] = useState<boolean>(false);
export default function Login({ query }) {
const [nametag, setNametag] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [showError, setShowError] = useState(false);
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
const onSubmit = async (e) => {
e.preventDefault();
if (!password || nametag.split("#").length < 2) return;
@ -34,7 +33,7 @@ export default function Login({ query }: { query: ParsedUrlQuery }) {
setLoading(true);
try {
const { token }: { token: string } = await axios
const { token } = await axios
.post("/api/login", {
name,
tag,
@ -53,11 +52,11 @@ export default function Login({ query }: { query: ParsedUrlQuery }) {
}),
});
const location = query?.next?.toString() ?? "/";
const location = query.next ? query.next : "/";
/^https?:\/\/|^\/\//i.test(location)
? (window.location.href = location)
: Router.push(location);
} catch (error) {
} catch (err) {
setError("The nametag or password entered is incorrect.");
setShowError(true);
setLoading(false);
@ -140,6 +139,4 @@ export default function Login({ query }: { query: ParsedUrlQuery }) {
);
}
Login.getInitialProps = ({ query }) => {
return { query };
};
Login.getInitialProps = ({ query }) => ({ query });

+ 0
- 2
src/types/index.ts View File

@ -1,2 +0,0 @@
export * from "./session";
export * from "./user";

+ 0
- 5
src/types/session.ts View File

@ -1,5 +0,0 @@
export type Session = {
id: string;
token: string;
user: string;
};

+ 0
- 20
src/types/user.ts View File

@ -1,20 +0,0 @@
export type User = {
id: string;
name: string;
tag: string;
nickname: string;
plus: boolean;
createdAt: string;
reputation: number;
xp: {
total: number;
level: number;
levelXp: number;
levelXpMax: number;
levelProgress: number;
};
hasPassword: boolean;
stripeCustomerId: string;
country: string;
birth: { day: number; month: number; year: number; date: string };
};

+ 6
- 0
src/utils/getAddress.js View File

@ -0,0 +1,6 @@
export const getAddress = (req) => {
if (req.headers["x-forwarded-for"]) {
const ips = req.headers["x-forwarded-for"].split(", ");
return ips[ips.length - 1];
} else return req.connection.remoteAddress;
};

src/lib/user.tsx → src/utils/user.js View File

@ -1,5 +1,4 @@
import { createContext, useContext } from "react";
import { User } from "../types";
export const UserContext = createContext<User>(null);
export const UserContext = createContext(null);
export const useUser = () => useContext(UserContext);

+ 0
- 19
tsconfig.json View File

@ -1,19 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"module": "esnext"
},
"exclude": ["node_modules"],
"include": ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx"]
}

+ 0
- 5
yarn.lock View File

@ -3394,11 +3394,6 @@ mkdirp@^0.5.1, mkdirp@^0.5.3:
dependencies:
minimist "^1.2.5"
moment@^2.27.0:
version "2.27.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d"
integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==
move-concurrently@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"

Loading…
Cancel
Save