Improved login, signup and new post creating using js forms
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
parent
6340d248fd
commit
66b617326d
@ -0,0 +1,104 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import axios from "axios";
|
||||
|
||||
import { FC, FormEvent, useCallback, useState } from "react";
|
||||
|
||||
import { LoginCredentials } from "@models/login";
|
||||
import { Response } from "@models/response";
|
||||
|
||||
import { parseAxiosError, parseAxiosResponse } from "@utils/fetch";
|
||||
import { parseUserInputError } from "@utils/errors";
|
||||
|
||||
const LoginForm: FC = () => {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [rememberMe, setRememberMe] = useState(false);
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const login = useCallback(
|
||||
async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const parseUserInputResult = LoginCredentials.safeParse({
|
||||
username,
|
||||
password,
|
||||
rememberMe
|
||||
});
|
||||
|
||||
if (!parseUserInputResult.success) {
|
||||
setError(parseUserInputError(parseUserInputResult.error.message));
|
||||
return;
|
||||
}
|
||||
|
||||
const response: Response = await axios
|
||||
.post("/api/login", parseUserInputResult.data)
|
||||
.then(parseAxiosResponse)
|
||||
.catch(parseAxiosError);
|
||||
|
||||
if (response.ok) {
|
||||
router.push("/blog");
|
||||
} else {
|
||||
setError(JSON.stringify(response.error));
|
||||
}
|
||||
},
|
||||
[username, password, rememberMe, router]
|
||||
);
|
||||
|
||||
return (
|
||||
<form onSubmit={login}>
|
||||
<div className="form-group">
|
||||
<label className="form-label" htmlFor="email">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
className="form-input"
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
name="username"
|
||||
type="email"
|
||||
id="email"
|
||||
placeholder="mail@example.com"
|
||||
/>
|
||||
<label className="form-label" htmlFor="password">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
className="form-input"
|
||||
placeholder="Password"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
minLength={8}
|
||||
maxLength={128}
|
||||
name="password"
|
||||
type="password"
|
||||
id="password"
|
||||
/>
|
||||
<label className="form-checkbox">
|
||||
<input
|
||||
checked={rememberMe}
|
||||
onChange={() => setRememberMe((state) => !state)}
|
||||
name="rememberMe"
|
||||
type="checkbox"
|
||||
/>
|
||||
<i className="form-icon" /> Remember me
|
||||
</label>
|
||||
{error !== null && (
|
||||
<div className="toast toast-error">
|
||||
<button
|
||||
className="btn btn-clear float-right"
|
||||
onClick={() => setError(null)}
|
||||
/>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<input className="btn btn-primary" type="submit" value="Login" />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginForm;
|
@ -0,0 +1,111 @@
|
||||
import axios from "axios";
|
||||
|
||||
import { FC, FormEvent, useCallback, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { SignupCredentials } from "@models/signup";
|
||||
import { Response } from "@models/response";
|
||||
|
||||
import { parseAxiosError, parseAxiosResponse } from "@utils/fetch";
|
||||
import { parseUserInputError } from "@utils/errors";
|
||||
|
||||
const SignupForm: FC = () => {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const signup = useCallback(
|
||||
async (e: FormEvent): Promise<void> => {
|
||||
e.preventDefault();
|
||||
|
||||
const parseUserInputResult = SignupCredentials.safeParse({
|
||||
email,
|
||||
password,
|
||||
name: username
|
||||
});
|
||||
|
||||
if (!parseUserInputResult.success) {
|
||||
setError(parseUserInputError(parseUserInputResult.error.message));
|
||||
return;
|
||||
}
|
||||
|
||||
const response: Response = await axios
|
||||
.post("/api/signup", parseUserInputResult.data)
|
||||
.then(parseAxiosResponse)
|
||||
.catch(parseAxiosError);
|
||||
|
||||
if (response.ok) {
|
||||
router.push("/blog");
|
||||
} else {
|
||||
setError(JSON.stringify(response.error));
|
||||
}
|
||||
},
|
||||
[email, password, username, router]
|
||||
);
|
||||
|
||||
return (
|
||||
<form onSubmit={signup}>
|
||||
<div className="form-group">
|
||||
<label className="form-label" htmlFor="email">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
className="form-input"
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
name="email"
|
||||
type="email"
|
||||
id="email"
|
||||
placeholder="mail@example.com"
|
||||
/>
|
||||
<label className="form-label" htmlFor="password">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
className="form-input"
|
||||
placeholder="Password"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
minLength={8}
|
||||
maxLength={128}
|
||||
name="password"
|
||||
type="password"
|
||||
id="password"
|
||||
/>
|
||||
|
||||
<label className="form-label" htmlFor="password">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
className="form-input"
|
||||
placeholder="Full name"
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
maxLength={32}
|
||||
name="name"
|
||||
type="text"
|
||||
id="name"
|
||||
/>
|
||||
|
||||
{error !== null && (
|
||||
<div className="toast toast-error mt-2">
|
||||
<button
|
||||
className="btn btn-clear float-right"
|
||||
onClick={() => setError(null)}
|
||||
/>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input className="btn btn-primary mt-2" type="submit" value="Signup" />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignupForm;
|
@ -0,0 +1,27 @@
|
||||
import z from "zod";
|
||||
|
||||
interface Error {
|
||||
ok: false;
|
||||
error?: unknown;
|
||||
}
|
||||
|
||||
export const ErrorModel: z.ZodType<Error> = z.object({
|
||||
ok: z.literal(false),
|
||||
error: z.unknown()
|
||||
});
|
||||
|
||||
interface Ok {
|
||||
ok: true;
|
||||
data?: unknown;
|
||||
}
|
||||
|
||||
export const OkModel: z.ZodType<Ok> = z.object({
|
||||
ok: z.literal(true),
|
||||
data: z.unknown()
|
||||
});
|
||||
|
||||
export type Response = Ok | Error;
|
||||
|
||||
const ResponseModel: z.ZodType<Response> = ErrorModel.or(OkModel);
|
||||
|
||||
export default ResponseModel;
|
@ -0,0 +1,17 @@
|
||||
import { Response } from "@models/response";
|
||||
|
||||
import { NextApiHandler } from "next";
|
||||
|
||||
import { registrationIsEnabled } from "@utils/config";
|
||||
import { methodNotAllowed } from "@utils/errors";
|
||||
|
||||
const handler: NextApiHandler<Response> = (req, res) => {
|
||||
if (req.method?.toUpperCase() !== "GET") {
|
||||
res.status(405).json(methodNotAllowed);
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ ok: true, data: { registrationIsEnabled } });
|
||||
};
|
||||
|
||||
export default handler;
|
@ -1,137 +0,0 @@
|
||||
import { GetServerSideProps, NextPage } from "next";
|
||||
import { NextSeo } from "next-seo";
|
||||
|
||||
import Layout from "@components/Layout";
|
||||
|
||||
import multipleClassNames from "@utils/multipleClassNames";
|
||||
|
||||
import styles from "./login.module.scss";
|
||||
import { registrationIsEnabled } from "@utils/config";
|
||||
|
||||
const Login: NextPage<{ registrationEnabled: boolean }> = ({
|
||||
registrationEnabled
|
||||
}) => {
|
||||
return (
|
||||
<Layout>
|
||||
<NextSeo title="Login" />
|
||||
<div className={styles.body}>
|
||||
<div className="columns">
|
||||
<div
|
||||
className={`col-md-4 ${
|
||||
registrationEnabled ? "col-ml-auto" : "col-mx-auto"
|
||||
}`}
|
||||
>
|
||||
<h2 className={styles.title}>Login to blog</h2>
|
||||
<form action="/api/blog/login" method="post">
|
||||
<div className="form-group">
|
||||
<label className="form-label" htmlFor="email">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
className="form-input"
|
||||
name="username"
|
||||
type="email"
|
||||
id="email"
|
||||
placeholder="mail@example.com"
|
||||
/>
|
||||
<label className="form-label" htmlFor="password">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
className="form-input"
|
||||
placeholder="Password"
|
||||
minLength={8}
|
||||
maxLength={128}
|
||||
name="password"
|
||||
type="password"
|
||||
id="password"
|
||||
/>
|
||||
<label className="form-checkbox">
|
||||
<input name="rememberMe" type="checkbox" />
|
||||
<i className="form-icon" /> Remember me
|
||||
</label>
|
||||
<input
|
||||
className={multipleClassNames(
|
||||
"btn btn-primary",
|
||||
styles.loginButton
|
||||
)}
|
||||
type="submit"
|
||||
value="Login"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{registrationEnabled && (
|
||||
<>
|
||||
<div
|
||||
className={multipleClassNames("divider-vert", styles.divider)}
|
||||
data-content="OR"
|
||||
/>
|
||||
<div className="col-md-4 col-mr-auto">
|
||||
<h2 className={styles.title}>Create new account</h2>
|
||||
<form action="/api/blog/signup" method="post">
|
||||
<div className="form-group">
|
||||
<label className="form-label" htmlFor="email">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
className="form-input"
|
||||
name="email"
|
||||
type="email"
|
||||
id="email"
|
||||
placeholder="mail@example.com"
|
||||
/>
|
||||
<label className="form-label" htmlFor="password">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
className="form-input"
|
||||
placeholder="Password"
|
||||
minLength={8}
|
||||
maxLength={128}
|
||||
name="password"
|
||||
type="password"
|
||||
id="password"
|
||||
/>
|
||||
|
||||
<label className="form-label" htmlFor="password">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
className="form-input"
|
||||
placeholder="Full name"
|
||||
maxLength={32}
|
||||
name="name"
|
||||
type="text"
|
||||
id="name"
|
||||
/>
|
||||
|
||||
<input
|
||||
className={multipleClassNames(
|
||||
"btn btn-primary",
|
||||
styles.signupButton
|
||||
)}
|
||||
type="submit"
|
||||
value="Signup"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async () => {
|
||||
return { props: { registrationEnabled: registrationIsEnabled } };
|
||||
};
|
||||
|
||||
export default Login;
|
@ -0,0 +1,87 @@
|
||||
import z from "zod";
|
||||
|
||||
import axios from "axios";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { NextPage } from "next";
|
||||
import { NextSeo } from "next-seo";
|
||||
|
||||
import Layout from "@components/Layout";
|
||||
|
||||
import multipleClassNames from "@utils/multipleClassNames";
|
||||
import { parseAxiosError, parseAxiosResponse } from "@utils/fetch";
|
||||
|
||||
import styles from "./login.module.scss";
|
||||
|
||||
import LoginForm from "@components/LoginForm";
|
||||
import SignupForm from "@components/SignupForm";
|
||||
|
||||
const SettingsModel = z.object({ registrationIsEnabled: z.boolean() });
|
||||
|
||||
const Login: NextPage = () => {
|
||||
const { data, isLoading, error } = useQuery<
|
||||
z.infer<typeof SettingsModel>,
|
||||
string
|
||||
>(["settings"], async (): Promise<z.infer<typeof SettingsModel>> => {
|
||||
const response = await axios
|
||||
.get("/api/settings")
|
||||
.then(parseAxiosResponse)
|
||||
.catch(parseAxiosError);
|
||||
|
||||
if (!response.ok) {
|
||||
throw JSON.stringify(response.error);
|
||||
}
|
||||
|
||||
const parseSettingsResult = SettingsModel.safeParse(response.data);
|
||||
|
||||
if (!parseSettingsResult.success) {
|
||||
throw parseSettingsResult.error.message;
|
||||
}
|
||||
|
||||
return parseSettingsResult.data;
|
||||
});
|
||||
|
||||
const registrationEnabled =
|
||||
data !== undefined && data.registrationIsEnabled && error === null;
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<NextSeo title="Login" />
|
||||
<div className={styles.body}>
|
||||
<div className="columns">
|
||||
<div
|
||||
className={`col-md-4 ${
|
||||
registrationEnabled ? "col-ml-auto" : "col-mx-auto"
|
||||
}`}
|
||||
>
|
||||
<h2 className={styles.title}>Login to blog</h2>
|
||||
<LoginForm />
|
||||
</div>
|
||||
|
||||
{registrationEnabled && (
|
||||
<>
|
||||
<div
|
||||
className={multipleClassNames("divider-vert", styles.divider)}
|
||||
data-content="OR"
|
||||
/>
|
||||
<div className="col-md-4 col-mr-auto">
|
||||
<h2 className={styles.title}>Create new account</h2>
|
||||
<SignupForm />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="columns">
|
||||
<div className="col col-mx-auto">
|
||||
<div className="loading loading-lg" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
@ -1,8 +1,17 @@
|
||||
const baseError = <T>(error: T) => ({ error, ok: false });
|
||||
import z from "zod";
|
||||
|
||||
export const methodNotAllowed = baseError("Method not allowed");
|
||||
import { Response } from "@models/response";
|
||||
|
||||
export const unauthorized = {
|
||||
ok: false,
|
||||
error: "Could not login; incorrect email or password"
|
||||
};
|
||||
const baseError = (error: string): Response => ({
|
||||
error,
|
||||
ok: false
|
||||
});
|
||||
|
||||
export const methodNotAllowed: Response = baseError("Method not allowed");
|
||||
|
||||
export const unauthorized: Response = baseError(
|
||||
"Could not login; incorrect email or password"
|
||||
);
|
||||
|
||||
export const parseUserInputError: (error: unknown) => string = (error) =>
|
||||
"Failed to parse user input: ".concat(error);
|
||||
|
@ -1,53 +1,23 @@
|
||||
import axios from "axios";
|
||||
import z from "zod";
|
||||
import { AxiosError, AxiosResponse } from "axios";
|
||||
|
||||
import createConfigCatClient from "./createConfigCatClient";
|
||||
import ResponseModel, { ErrorModel, Response } from "@models/response";
|
||||
|
||||
import {
|
||||
RepositoryResponse,
|
||||
SearchResultsResponse,
|
||||
UserResponse
|
||||
} from "@models/responses";
|
||||
import { giteaServerUrl } from "./config";
|
||||
export const parseAxiosResponse = (res: AxiosResponse<unknown>): Response => {
|
||||
const parseResponseResult = ResponseModel.safeParse(res.data);
|
||||
|
||||
const apiUrl = `https://${giteaServerUrl}/api/v1`;
|
||||
if (!parseResponseResult.success) {
|
||||
return { ok: false, error: parseResponseResult.error };
|
||||
}
|
||||
|
||||
export const fetchAvailability = async (): Promise<boolean> => {
|
||||
const configCatClient = createConfigCatClient();
|
||||
|
||||
const isAvailable: boolean =
|
||||
(await configCatClient?.getValueAsync("amiavailable", true)) ?? true;
|
||||
|
||||
return isAvailable;
|
||||
};
|
||||
|
||||
export const fetchUser = async (
|
||||
giteaUsername: string
|
||||
): Promise<z.infer<typeof UserResponse>> => {
|
||||
const { data: user } = await axios.get<unknown>(
|
||||
`${apiUrl}/users/${giteaUsername}`
|
||||
);
|
||||
|
||||
return UserResponse.parse(user);
|
||||
return parseResponseResult.data;
|
||||
};
|
||||
|
||||
export const fetchRepositories = async (
|
||||
giteaUserUid: number
|
||||
): Promise<z.infer<typeof RepositoryResponse>[]> => {
|
||||
const { data: repositories } = await axios.get<unknown>(
|
||||
`${apiUrl}/repos/search`,
|
||||
{
|
||||
params: {
|
||||
topic: true,
|
||||
q: "on-portfolio",
|
||||
id: giteaUserUid,
|
||||
limit: 6
|
||||
}
|
||||
}
|
||||
);
|
||||
export const parseAxiosError = (e: AxiosError<unknown>): Response => {
|
||||
const parseErrorResult = ErrorModel.safeParse(e.response?.data);
|
||||
|
||||
const results = SearchResultsResponse.parse(repositories);
|
||||
if (!parseErrorResult.success) {
|
||||
return { ok: false, error: parseErrorResult.error };
|
||||
}
|
||||
|
||||
if (results.ok) return results.data;
|
||||
else throw results.data;
|
||||
return parseErrorResult.data;
|
||||
};
|
||||
|
@ -0,0 +1,54 @@
|
||||
import axios from "axios";
|
||||
import z from "zod";
|
||||
|
||||
import createConfigCatClient from "@utils/createConfigCatClient";
|
||||
|
||||
import {
|
||||
RepositoryResponse,
|
||||
SearchResultsResponse,
|
||||
UserResponse
|
||||
} from "@models/git/responses";
|
||||
|
||||
import { giteaServerUrl } from "@utils/config";
|
||||
|
||||
const apiUrl = `https://${giteaServerUrl}/api/v1`;
|
||||
|
||||
export const fetchAvailability = async (): Promise<boolean> => {
|
||||
const configCatClient = createConfigCatClient();
|
||||
|
||||
const isAvailable: boolean =
|
||||
(await configCatClient?.getValueAsync("amiavailable", true)) ?? true;
|
||||
|
||||
return isAvailable;
|
||||
};
|
||||
|
||||
export const fetchUser = async (
|
||||
giteaUsername: string
|
||||
): Promise<z.infer<typeof UserResponse>> => {
|
||||
const { data: user } = await axios.get<unknown>(
|
||||
`${apiUrl}/users/${giteaUsername}`
|
||||
);
|
||||
|
||||
return UserResponse.parse(user);
|
||||
};
|
||||
|
||||
export const fetchRepositories = async (
|
||||
giteaUserUid: number
|
||||
): Promise<z.infer<typeof RepositoryResponse>[]> => {
|
||||
const { data: repositories } = await axios.get<unknown>(
|
||||
`${apiUrl}/repos/search`,
|
||||
{
|
||||
params: {
|
||||
topic: true,
|
||||
q: "on-portfolio",
|
||||
id: giteaUserUid,
|
||||
limit: 6
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const results = SearchResultsResponse.parse(repositories);
|
||||
|
||||
if (results.ok) return results.data;
|
||||
else throw results.data;
|
||||
};
|
Loading…
Reference in new issue