materia-web-client: unify style with catppuccin

materia-server: fixing auth
This commit is contained in:
L-Nafaryus 2024-06-20 00:11:35 +05:00
parent 997f37d5ee
commit d8b19da646
Signed by: L-Nafaryus
GPG Key ID: 553C97999B363D38
31 changed files with 349 additions and 205 deletions

View File

@ -219,7 +219,9 @@
};
devShells.x86_64-linux.default = pkgs.mkShell {
buildInputs = with pkgs; [ postgresql redis ];
buildInputs = with pkgs; [ postgresql redis pdm nodejs ];
# greenlet requires libstdc++
LD_LIBRARY_PATH = nixpkgs.lib.makeLibraryPath [ pkgs.stdenv.cc.cc ];
};
};
}

View File

@ -46,8 +46,8 @@ materia-server = "materia_server.main:server"
[tool.pdm.scripts]
start-server.cmd = "python ./src/materia_server/main.py {args:start --app-mode development --log-level debug}"
db-upgrade.cmd = "alembic upgrade {args:head}"
db-downgrade.cmd = "alembic downgrade {args:base}"
db-upgrade.cmd = "alembic -c ./src/materia_server/alembic.ini upgrade {args:head}"
db-downgrade.shell = "alembic -c ./src/materia_server/alembic.ini downgrade {args:base}"
db-revision.cmd = "alembic revision {args:--autogenerate}"
remove-revisions.shell = "rm -v ./src/materia_server/models/migrations/versions/*.py"

View File

@ -1 +1 @@
from materia_server.models.user.user import User, UserCredentials
from materia_server.models.user.user import User, UserCredentials, UserIdentity

View File

@ -3,7 +3,7 @@ from typing import Optional
import time
import re
from pydantic import BaseModel, EmailStr
from pydantic import BaseModel, EmailStr, ConfigDict
import pydantic
from sqlalchemy import BigInteger, Enum
from sqlalchemy.orm import mapped_column, Mapped, relationship
@ -23,7 +23,7 @@ class User(Base):
id: Mapped[UUID] = mapped_column(primary_key = True, default = uuid4)
name: Mapped[str] = mapped_column(unique = True)
lower_name: Mapped[str] = mapped_column(unique = True)
full_name: Mapped[str]
full_name: Mapped[Optional[str]]
email: Mapped[str]
is_email_private: Mapped[bool] = mapped_column(default = True)
hashed_password: Mapped[str]
@ -80,5 +80,25 @@ class UserCredentials(BaseModel):
password: str
email: Optional[EmailStr]
class UserIdentity(BaseModel):
model_config = ConfigDict(from_attributes = True)
name: str
lower_name: str
full_name: Optional[str]
email: str
is_email_private: bool
must_change_password: bool
login_type: "LoginType"
created: int
updated: int
last_login: Optional[int]
is_active: bool
is_admin: bool
avatar: Optional[str]
from materia_server.models.repository.repository import Repository

View File

@ -1 +1,2 @@
from materia_server.routers import api
from materia_server.routers import middleware

View File

@ -1,6 +1,8 @@
from fastapi import APIRouter
from materia_server.routers.api import auth
from materia_server.routers.api import user
router = APIRouter(prefix = "/api")
router.include_router(auth.router)
router.include_router(user.router)

View File

@ -40,8 +40,9 @@ async def signup(body: user.UserCredentials, ctx: context.Context = Depends()):
@router.post("/auth/signin")
async def signin(body: user.UserCredentials, response: Response, ctx: context.Context = Depends()):
if (current_user := await user.User.by_name(body.name, ctx.database) or await user.User.by_email(body.email, ctx.database) if body.email else None) is None:
raise HTTPException(status_code = status.HTTP_401_UNAUTHORIZED, detail = "Invalid credentials")
if (current_user := await user.User.by_name(body.name, ctx.database)) is None:
if (current_user := await user.User.by_email(str(body.email), ctx.database)) is None:
raise HTTPException(status_code = status.HTTP_401_UNAUTHORIZED, detail = "Invalid email")
if not security.validate_password(body.password, current_user.hashed_password, algo = ctx.config.security.password_hash_algo):
raise HTTPException(status_code = status.HTTP_401_UNAUTHORIZED, detail = "Invalid password")

View File

@ -0,0 +1,5 @@
from fastapi import APIRouter
from materia_server.routers.api.user import user
router = APIRouter()
router.include_router(user.router)

View File

@ -0,0 +1,19 @@
from typing import Optional
import uuid
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
from materia_server import security
from materia_server.routers import context
from materia_server.models import user
from materia_server.models import auth
from materia_server.routers.middleware import JwtMiddleware
router = APIRouter(tags = ["user"])
@router.get("/user/identity", response_model = user.UserIdentity)
async def identity(request: Request, claims = Depends(JwtMiddleware()), ctx: context.Context = Depends()):
if not (current_user := await user.User.by_id(uuid.UUID(claims.sub), ctx.database)):
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing user")
return user.UserIdentity.model_validate(current_user)

View File

@ -1,45 +1,79 @@
from typing import Optional, Sequence
import uuid
from fastapi import HTTPException, Request, Response, status, Depends
from fastapi import HTTPException, Request, Response, status, Depends, Cookie
from fastapi.security.base import SecurityBase
import jwt
from sqlalchemy import select
from pydantic import BaseModel
from enum import StrEnum
from http import HTTPMethod as HttpMethod
from fastapi.security import HTTPBearer, OAuth2PasswordBearer, OAuth2PasswordRequestForm, APIKeyQuery, APIKeyCookie, APIKeyHeader
from fastapi.security import HTTPBearer, OAuth2PasswordBearer, OAuth2PasswordRequestForm, APIKeyQuery, APIKeyCookie, APIKeyHeader
from materia.api.state import ConfigState, DatabaseState
from materia.api.token import TokenClaims
from materia import db
from materia_server import security
from materia_server.routers import context
from materia_server.models import user
class JwtMiddleware(HTTPBearer):
def __init__(self, auto_error: bool = True):
super().__init__(auto_error = auto_error)
self.claims: Optional[TokenClaims] = None
async def get_token_claims(token, ctx: context.Context = Depends()) -> security.TokenClaims:
try:
secret = ctx.config.oauth2.jwt_secret if ctx.config.oauth2.jwt_signing_algo in ["HS256", "HS384", "HS512"] else ctx.config.oauth2.jwt_signing_key
claims = security.validate_token(token, secret)
user_id = uuid.UUID(claims.sub) # type: ignore
except jwt.PyJWKError as _:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid token")
except ValueError as _:
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Invalid token")
async def __call__(self, request: Request, config: ConfigState = Depends(), database: DatabaseState = Depends()):
if token := request.cookies.get("token"):
pass
elif (credentials := await super().__call__(request)) and credentials.scheme == "Bearer":
if not (current_user := await user.User.by_id(user_id, ctx.database)):
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing user")
return claims
class JwtBearer(HTTPBearer):
def __init__(self, **kwargs):
super().__init__(scheme_name = "Bearer", **kwargs)
self.claims = None
async def __call__(self, request: Request, ctx: context.Context = Depends()):
if credentials := await super().__call__(request):
token = credentials.credentials
if not token:
else:
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Missing token")
self.claims = await get_token_claims(token, ctx)
class JwtCookie(SecurityBase):
def __init(self, *, auto_error: bool = True):
self.auto_error = auto_error
self.claims = None
async def __call__(self, request: Request, response: Response, ctx: context.Context = Depends()):
if not (access_token := request.cookies.get(ctx.config.security.cookie_access_token_name)):
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing token")
refresh_token = request.cookies.get(ctx.config.security.cookie_refresh_token_name)
if ctx.config.oauth2.jwt_signing_algo in ["HS256", "HS384", "HS512"]:
secret = ctx.config.oauth2.jwt_secret
else:
secret = ctx.config.oauth2.jwt_signing_key
try:
self.claims = TokenClaims.verify(token, config.jwt.secret)
user_id = uuid.UUID(self.claims.sub) # type: ignore
except jwt.PyJWKError as _:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid token")
except ValueError as _:
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Invalid token")
refresh_claims = security.validate_token(refresh_token, secret) if refresh_token else None
# TODO: check expiration
except jwt.PyJWTError:
refresh_claims = None
async with database.session() as session:
if not (user := (await session.scalars(select(db.User).where(db.User.id == user_id))).first()):
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing user")
try:
access_claims = security.validate_token(access_token, secret)
# TODO: if exp then check refresh token and create new else raise
except jwt.PyJWTError as e:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, f"Invalid token: {e}")
else:
# TODO: validate user
pass
self.claims = access_claims
request.state.user = user
WILDCARD = "*"
NULL = "null"

View File

@ -8,6 +8,7 @@
"name": "materia-frontend",
"version": "0.0.1",
"dependencies": {
"@catppuccin/tailwindcss": "^0.1.6",
"autoprefixer": "^10.4.18",
"axios": "^1.6.8",
"pinia": "^2.1.7",
@ -484,6 +485,14 @@
"node": ">=6.9.0"
}
},
"node_modules/@catppuccin/tailwindcss": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@catppuccin/tailwindcss/-/tailwindcss-0.1.6.tgz",
"integrity": "sha512-V+Y0AwZ5SSyvOVAcDl7Ng30xy+m82OKnEJ+9+kcZZ7lRyXuZrAb2GScdq9XR3v+ggt8qiZ/G4TvaC9cJ88AAXA==",
"peerDependencies": {
"tailwindcss": ">=3.0.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
@ -1494,11 +1503,11 @@
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dependencies": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@ -1859,9 +1868,9 @@
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dependencies": {
"to-regex-range": "^5.0.1"
},

View File

@ -11,6 +11,7 @@
"type-check": "vue-tsc --build --force"
},
"dependencies": {
"@catppuccin/tailwindcss": "^0.1.6",
"autoprefixer": "^10.4.18",
"axios": "^1.6.8",
"pinia": "^2.1.7",

View File

@ -0,0 +1,23 @@
import { client, type ResponseError, handle_error } from "@/api/client";
export interface UserCredentials {
name: string,
password: string,
email?: string
}
export async function signup(body: UserCredentials): Promise<null | ResponseError> {
return await client.post("/auth/signup", JSON.stringify(body))
.catch(handle_error);
}
export async function signin(body: UserCredentials): Promise<null | ResponseError> {
return await client.post("/auth/signin", JSON.stringify(body))
.catch(handle_error);
}
export async function signout(): Promise<null | ResponseError> {
return await client.post("/auth/signout")
.catch(handle_error);
}

View File

@ -13,12 +13,19 @@ export class HttpError extends Error {
}
export interface ResponseError {
status_code: number,
message: string
status: number | null,
message: string | null
}
export function handle_error(error: AxiosError): Promise<ResponseError> {
return Promise.reject<ResponseError>({ status_code: error.response?.status, message: error.response?.data });
let message = error.response?.data?.detail || error.response?.data;
console.log(error);
// extract pydantic error message
if (error.response.status == 422) {
message = error.response?.data?.detail[1].ctx.reason;
}
return Promise.reject<ResponseError>({ status: error.response.status, message: message});
}
const debug = import.meta.hot;

View File

@ -1 +1,2 @@
export * as auth from "@/api/auth";
export * as user from "@/api/user";

View File

@ -4,24 +4,37 @@
@layer base {
body {
background-color: rgba(40, 30, 30, 1); /*linear-gradient(rgba(36, 14, 84, 1) 80%, rgba(55, 22, 130, 1)); */
/*background-image: url("./background.svg");*/
background-position: left top;
background-repeat: repeat-x;
@apply bg-ctp-crust;
font-family: Inter,sans-serif;
font-weight: 400;
}
a {
@apply text-green-500 hover:text-green-400;
@apply text-ctp-green;
}
h1 {
font-family: BioRhyme,serif;
font-weight: 700;
.input {
@apply w-full pl-3 pr-3 pt-2 pb-2 rounded border bg-ctp-mantle border-ctp-overlay0 hover:border-ctp-overlay1 focus:border-ctp-green text-ctp-text outline-none;
}
label {
font-family: Space Mono,monospace;
font-weight: 500;
@apply text-ctp-text;
}
.button {
@apply pt-2 pb-2 pl-5 pr-5 rounded bg-ctp-mantle hover:bg-ctp-base text-ctp-blue;
}
.link-button {
@apply button text-ctp-green;
}
h1 {
@apply text-ctp-text pt-5 pb-5 border-b border-ctp-overlay0 mb-5;
}
label {
@apply text-ctp-text;
}
}

View File

@ -0,0 +1,5 @@
<template>
<h1 class="text-center pt-3 pb-3 bg-ctp-red/25 rounded border border-ctp-red text-ctp-red">
<slot></slot>
</h1>
</template>

View File

@ -1,7 +1,7 @@
<template>
<div class="relative h-12 border-b border-b-zinc-500">
<div class="relative h-12">
<nav
class="absolute w-full h-[calc(100%-1px)] flex justify-between items-center m-0 pl-3 pr-3 bg-gradient-to-t from-zinc-800 to-zinc-900">
class="absolute w-full h-full flex justify-between items-center m-0 pl-3 pr-3 bg-ctp-mantle">
<div class="items-center m-0 flex">
<slot name="left"></slot>
</div>

View File

@ -0,0 +1,5 @@
<template>
<h1 class="text-center pt-3 pb-3 bg-ctp-peach/25 rounded border border-ctp-peach text-ctp-peach">
<slot></slot>
</h1>
</template>

View File

@ -1,5 +0,0 @@
<template>
<h1 class="text-center pt-3 pb-3 bg-orange-900 rounded border border-orange-700">
<slot></slot>
</h1>
</template>

View File

@ -0,0 +1 @@
export * as api from "@/api";

View File

@ -47,12 +47,12 @@ const router = createRouter({
component: () => import("@/views/Home.vue"),
},
{
path: "/user/login", name: "signin", beforeEnter: [bypass_auth],
component: () => import("@/views/user/SignIn.vue")
path: "/auth/signin", name: "signin", beforeEnter: [bypass_auth],
component: () => import("@/views/auth/SignIn.vue")
},
{
path: "/user/register", name: "signup", //beforeEnter: [bypass_auth],
component: () => import("@/views/user/SignUp.vue")
path: "/auth/signup", name: "signup", //beforeEnter: [bypass_auth],
component: () => import("@/views/auth/SignUp.vue")
},
{
path: "/user/preferencies", name: "prefs", redirect: { name: "prefs-profile" }, beforeEnter: [required_auth],
@ -78,7 +78,7 @@ const router = createRouter({
},
{
path: "/:pathMatch(.*)*", name: "not-found", beforeEnter: [bypass_auth],
component: () => import("@/views/error/NotFound.vue")
component: () => import("@/views/NotFound.vue")
}
]
});

View File

@ -26,7 +26,7 @@ async function signout() {
<div class="flex-grow pb-20">
<NavBar>
<template #left>
<!-- TODO: logo -->
<RouterLink class="link-button" to="/">Home</RouterLink>
</template>
<template #right>
<DropdownMenu v-if="userStore.current">
@ -57,23 +57,23 @@ async function signout() {
</template>
</DropdownMenu>
<RouterLink v-if="!userStore.current"
class="flex min-w-9 min-h-9 pt-1 pb-1 pl-3 pr-3 rounded hover:bg-zinc-600" to="/user/login">
Sign In</RouterLink>
<RouterLink v-if="!userStore.current" class="link-button" to="/user/login">Sign In</RouterLink>
</template>
</NavBar>
<main>
<main class="w-[1000px] ml-auto mr-auto pt-5 pb-5">
<slot></slot>
</main>
</div>
<div class="relative overflow-hidden h-full ">
</div>
<footer
class="flex justify-between pb-2 pt-2 pl-5 pr-5 bg-gradient-to-b from-zinc-800 to-zinc-900 border-t border-t-zinc-500">
<a href="/">Made with glove</a>
class="flex justify-between pb-2 pt-2 pl-5 pr-5 bg-ctp-mantle">
<a href="/">Made with glove by Elnafo, 2024</a>
<div>
</div>
<a href="/api/docs">API</a>
</footer>
</template>

View File

@ -1,9 +1,11 @@
<script setup lang="ts">
import Base from '@/views/Base.vue';
import Base from "@/views/Base.vue";
</script>
<template>
<Base>
Home
<h1 class="text-2xl">Easy and secure cloud storage</h1>
<h1 class="text-2xl">Run Materia anywhere</h1>
<h1 class="text-2xl">Manage files with CLI application</h1>
</Base>
</template>

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import Base from "@/views/Base.vue";
import Error from "@/components/error/Error.vue";
import Error from "@/components/Error.vue";
</script>
<template>

View File

@ -0,0 +1,71 @@
<script setup lang="ts">
import Base from "@/views/Base.vue";
import Error from "@/components/Error.vue";
import { ref, onMounted } from "vue";
import router from "@/router";
import { api } from "@";
import { useUserStore } from "@/stores";
const email_or_username = defineModel("email_or_username");
const password = defineModel("password");
const userStore = useUserStore();
const error = ref(null);
onMounted(async () => {
if (userStore.current) {
router.replace({ path: "/" });
}
});
async function signin() {
if (!email_or_username.value || !password.value) {
return;
}
const body: user.UserCredentials = {
name: null,
password: password.value,
email: null
};
if (/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(email_or_username.value)) {
body.name = "";
body.email = email_or_username.value;
} else {
body.name = email_or_username.value;
}
await api.auth.signin(body)
.then(async () => {
//userStore.current = user;
router.push({ path: "/" });
})
.catch(err => { error.value = err.message; });
};
</script>
<template>
<Base>
<div class="ml-auto mr-auto w-1/2 pt-5 pb-5">
<h1 class="text-center text-ctp-text pt-5 pb-5 border-b border-ctp-overlay0 mb-5">Sign In</h1>
<form @submit.prevent>
<div class="mb-5">
<label for="email_or_login">Email or Username</label>
<input v-model="email_or_username" placeholder="" name="email_or_login" required class="input">
</div>
<div class="mb-5">
<label for="password">Password</label>
<input v-model="password" placeholder="" type="password" name="password" required class="input">
</div>
<div class="mb-5 flex justify-between items-center">
<button @click="signin" class="button">Sign In</button>
<button @click="$router.push('/user/register')" class="button">Sign Up</button>
</div>
</form>
<Error v-if="error">{{ error }}</Error>
</div>
</Base>
</template>

View File

@ -0,0 +1,50 @@
<script setup lang="ts">
import Base from "@/views/Base.vue";
import Error from "@/components/Error.vue";
import { ref } from "vue";
import router from "@/router";
import { api } from "@";
const login = defineModel("login");
const email = defineModel("email");
const password = defineModel("password");
const error = ref(null);
async function signup() {
if (!login.value || !email.value || !password.value) {
return;
}
await api.auth.signup({ name: login.value, password: password.value, email: email.value })
.then(async user => { router.push({ path: "/user/login" }); })
.catch(err => { error.value = err.message; });
};
</script>
<template>
<Base>
<div class="ml-auto mr-auto w-1/2 pt-5 pb-5">
<h1 class="text-center">Sign Up</h1>
<form @submit.prevent>
<div class="mb-5">
<label for="login">Login</label>
<input v-model="login" type="" placeholder="" name="login" required class="input">
</div>
<div class="mb-5">
<label for="email">Email Address</label>
<input v-model="email" type="email" placeholder="" name="email" required class="input">
</div>
<div class="mb-5">
<label for="password">Password</label>
<input v-model="password" placeholder="" type="password" name="password" required class="input">
</div>
<div class="mb-5 flex justify-between items-center">
<button @click="signup" class="button">Sign Up</button>
</div>
</form>
<Error v-if="error">{{ error }}</Error>
</div>
</Base>
</template>

View File

@ -1,76 +0,0 @@
<script setup lang="ts">
import Base from "@/views/Base.vue";
import Error from "@/components/error/Error.vue";
import { ref, onMounted } from "vue";
import router from "@/router";
import { user } from "@/api";
import { useUserStore } from "@/stores";
const email_or_username = defineModel("email_or_username");
const password = defineModel("password");
const userStore = useUserStore();
const error = ref(null);
onMounted(async () => {
if (userStore.current) {
router.replace({ path: "/" });
}
});
async function signin() {
const body: user.UserCredentials = {
name: null,
password: password.value,
email: null
};
if (/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(email_or_username.value)) {
body.name = "";
body.email = email_or_username.value;
} else {
body.name = email_or_username.value;
}
await user.signin(body)
.then(async () => {
//userStore.current = user;
router.push({ path: "/" });
})
.catch(error => { error.value = error; });
};
</script>
<template>
<Base>
<div class="ml-auto mr-auto w-1/2 pt-5 pb-5">
<h1 class="text-center pt-5 pb-5 border-b border-zinc-500">Sign In</h1>
<form @submit.prevent class="m-auto pt-5 pb-5">
<div class="mb-5 ml-auto mr-auto">
<label for="email_or_login" class="text-right w-64 inline-block mr-5">Email or Username</label>
<input v-model="email_or_username" placeholder="" name="email_or_login" required
class="w-1/2 bg-zinc-800 pl-3 pr-3 pt-2 pb-2 outline-none rounded border border-zinc-500 hover:border-zinc-400 focus:border-green-800">
</div>
<div class="mb-5 ml-auto mr-auto">
<label for="password" class="text-right w-64 inline-block mr-5">Password</label>
<input v-model="password" placeholder="" type="password" name="password" required
class="w-1/2 bg-zinc-800 pl-3 pr-3 pt-2 pb-2 outline-none rounded border border-zinc-500 hover:border-zinc-400 focus:border-green-800">
</div>
<div class="mb-5 ml-auto mr-auto">
<label class="text-right w-64 inline-block mr-5"></label>
<div class="flex justify-between items-center w-1/2 m-auto">
<button @click="signin" class="rounded bg-zinc-500 hover:bg-zinc-400 pb-2 pt-2 pl-5 pr-5">Sign
In</button>
<p>or</p>
<button @click="$router.push('/user/register')"
class="rounded bg-zinc-500 hover:bg-zinc-400 pb-2 pt-2 pl-5 pr-5">Sign
Up</button>
</div>
</div>
</form>
<Error v-if="error">{{ error }}</Error>
</div>
</Base>
</template>

View File

@ -1,52 +0,0 @@
<script setup lang="ts">
import Base from "@/views/Base.vue";
import Error from "@/components/error/Error.vue";
import { ref } from "vue";
import router from "@/router";
import { user } from "@/api";
const login = defineModel("login");
const email = defineModel("email");
const password = defineModel("password");
const error = ref(null);
async function signup() {
await user.register({ login: login.value, password: password.value, email: email.value })
.then(async user => { router.push({ path: "/user/login" }); })
.catch(error => { error.value = error; });
};
</script>
<template>
<Base>
<div class="ml-auto mr-auto w-1/2 pt-5 pb-5">
<h4 class="text-center pt-5 pb-5 border-b border-zinc-500">Sign Up</h4>
<form @submit.prevent class="m-auto pt-5 pb-5">
<div class="mb-5 ml-auto mr-auto">
<label for="login" class="text-right w-64 inline-block mr-5">Login</label>
<input v-model="login" type="" placeholder="" name="login" required
class="w-1/2 bg-zinc-800 pl-3 pr-3 pt-2 pb-2 outline-none rounded border border-zinc-500 hover:border-zinc-400 focus:border-green-800">
</div>
<div class="mb-5 ml-auto mr-auto">
<label for="email" class="text-right w-64 inline-block mr-5">Email Address</label>
<input v-model="email" type="email" placeholder="" name="email" required
class="w-1/2 bg-zinc-800 pl-3 pr-3 pt-2 pb-2 outline-none rounded border border-zinc-500 hover:border-zinc-400 focus:border-green-800">
</div>
<div class="mb-5 ml-auto mr-auto">
<label for="password" class="text-right w-64 inline-block mr-5">Password</label>
<input v-model="password" placeholder="" type="password" name="password" required
class="w-1/2 bg-zinc-800 pl-3 pr-3 pt-2 pb-2 outline-none rounded border border-zinc-500 hover:border-zinc-400 focus:border-green-800">
</div>
<div class="mb-5 ml-auto mr-auto">
<label class="text-right w-64 inline-block mr-5"></label>
<button @click="signup" class="rounded bg-zinc-500 hover:bg-zinc-400 pb-2 pt-2 pl-5 pr-5">Sign
Up</button>
</div>
</form>
<Error v-if="error">{{ error.message }}</Error>
</div>
</Base>
</template>

View File

@ -21,7 +21,12 @@ export default {
}
},
},
plugins: [],
plugins: [
require("@catppuccin/tailwindcss")({
prefix: "ctp",
defaultFlavour: "macchiato"
})
],
}