materia-web-client: unify style with catppuccin
materia-server: fixing auth
This commit is contained in:
parent
997f37d5ee
commit
d8b19da646
@ -219,7 +219,9 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
devShells.x86_64-linux.default = pkgs.mkShell {
|
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 ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -46,8 +46,8 @@ materia-server = "materia_server.main:server"
|
|||||||
|
|
||||||
[tool.pdm.scripts]
|
[tool.pdm.scripts]
|
||||||
start-server.cmd = "python ./src/materia_server/main.py {args:start --app-mode development --log-level debug}"
|
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-upgrade.cmd = "alembic -c ./src/materia_server/alembic.ini upgrade {args:head}"
|
||||||
db-downgrade.cmd = "alembic downgrade {args:base}"
|
db-downgrade.shell = "alembic -c ./src/materia_server/alembic.ini downgrade {args:base}"
|
||||||
db-revision.cmd = "alembic revision {args:--autogenerate}"
|
db-revision.cmd = "alembic revision {args:--autogenerate}"
|
||||||
remove-revisions.shell = "rm -v ./src/materia_server/models/migrations/versions/*.py"
|
remove-revisions.shell = "rm -v ./src/materia_server/models/migrations/versions/*.py"
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
from materia_server.models.user.user import User, UserCredentials
|
from materia_server.models.user.user import User, UserCredentials, UserIdentity
|
||||||
|
@ -3,7 +3,7 @@ from typing import Optional
|
|||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from pydantic import BaseModel, EmailStr
|
from pydantic import BaseModel, EmailStr, ConfigDict
|
||||||
import pydantic
|
import pydantic
|
||||||
from sqlalchemy import BigInteger, Enum
|
from sqlalchemy import BigInteger, Enum
|
||||||
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
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)
|
id: Mapped[UUID] = mapped_column(primary_key = True, default = uuid4)
|
||||||
name: Mapped[str] = mapped_column(unique = True)
|
name: Mapped[str] = mapped_column(unique = True)
|
||||||
lower_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]
|
email: Mapped[str]
|
||||||
is_email_private: Mapped[bool] = mapped_column(default = True)
|
is_email_private: Mapped[bool] = mapped_column(default = True)
|
||||||
hashed_password: Mapped[str]
|
hashed_password: Mapped[str]
|
||||||
@ -80,5 +80,25 @@ class UserCredentials(BaseModel):
|
|||||||
password: str
|
password: str
|
||||||
email: Optional[EmailStr]
|
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
|
from materia_server.models.repository.repository import Repository
|
||||||
|
@ -1 +1,2 @@
|
|||||||
from materia_server.routers import api
|
from materia_server.routers import api
|
||||||
|
from materia_server.routers import middleware
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from materia_server.routers.api import auth
|
from materia_server.routers.api import auth
|
||||||
|
from materia_server.routers.api import user
|
||||||
|
|
||||||
router = APIRouter(prefix = "/api")
|
router = APIRouter(prefix = "/api")
|
||||||
|
|
||||||
router.include_router(auth.router)
|
router.include_router(auth.router)
|
||||||
|
router.include_router(user.router)
|
||||||
|
@ -40,8 +40,9 @@ async def signup(body: user.UserCredentials, ctx: context.Context = Depends()):
|
|||||||
|
|
||||||
@router.post("/auth/signin")
|
@router.post("/auth/signin")
|
||||||
async def signin(body: user.UserCredentials, response: Response, ctx: context.Context = Depends()):
|
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:
|
if (current_user := await user.User.by_name(body.name, ctx.database)) is None:
|
||||||
raise HTTPException(status_code = status.HTTP_401_UNAUTHORIZED, detail = "Invalid credentials")
|
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):
|
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")
|
raise HTTPException(status_code = status.HTTP_401_UNAUTHORIZED, detail = "Invalid password")
|
||||||
|
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
from materia_server.routers.api.user import user
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
router.include_router(user.router)
|
19
materia-server/src/materia_server/routers/api/user/user.py
Normal file
19
materia-server/src/materia_server/routers/api/user/user.py
Normal 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)
|
@ -1,6 +1,7 @@
|
|||||||
from typing import Optional, Sequence
|
from typing import Optional, Sequence
|
||||||
import uuid
|
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
|
import jwt
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@ -8,38 +9,71 @@ from enum import StrEnum
|
|||||||
from http import HTTPMethod as HttpMethod
|
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_server import security
|
||||||
from materia.api.token import TokenClaims
|
from materia_server.routers import context
|
||||||
from materia import db
|
from materia_server.models import user
|
||||||
|
|
||||||
|
|
||||||
class JwtMiddleware(HTTPBearer):
|
async def get_token_claims(token, ctx: context.Context = Depends()) -> security.TokenClaims:
|
||||||
def __init__(self, auto_error: bool = True):
|
|
||||||
super().__init__(auto_error = auto_error)
|
|
||||||
self.claims: Optional[TokenClaims] = None
|
|
||||||
|
|
||||||
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":
|
|
||||||
token = credentials.credentials
|
|
||||||
|
|
||||||
if not token:
|
|
||||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Missing token")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.claims = TokenClaims.verify(token, config.jwt.secret)
|
secret = ctx.config.oauth2.jwt_secret if ctx.config.oauth2.jwt_signing_algo in ["HS256", "HS384", "HS512"] else ctx.config.oauth2.jwt_signing_key
|
||||||
user_id = uuid.UUID(self.claims.sub) # type: ignore
|
claims = security.validate_token(token, secret)
|
||||||
|
user_id = uuid.UUID(claims.sub) # type: ignore
|
||||||
except jwt.PyJWKError as _:
|
except jwt.PyJWKError as _:
|
||||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid token")
|
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid token")
|
||||||
except ValueError as _:
|
except ValueError as _:
|
||||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Invalid token")
|
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Invalid token")
|
||||||
|
|
||||||
async with database.session() as session:
|
if not (current_user := await user.User.by_id(user_id, ctx.database)):
|
||||||
if not (user := (await session.scalars(select(db.User).where(db.User.id == user_id))).first()):
|
|
||||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing user")
|
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing user")
|
||||||
|
|
||||||
request.state.user = 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
|
||||||
|
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:
|
||||||
|
refresh_claims = security.validate_token(refresh_token, secret) if refresh_token else None
|
||||||
|
# TODO: check expiration
|
||||||
|
except jwt.PyJWTError:
|
||||||
|
refresh_claims = None
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
WILDCARD = "*"
|
WILDCARD = "*"
|
||||||
NULL = "null"
|
NULL = "null"
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
"name": "materia-frontend",
|
"name": "materia-frontend",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@catppuccin/tailwindcss": "^0.1.6",
|
||||||
"autoprefixer": "^10.4.18",
|
"autoprefixer": "^10.4.18",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
@ -484,6 +485,14 @@
|
|||||||
"node": ">=6.9.0"
|
"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": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.20.2",
|
"version": "0.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
|
||||||
@ -1494,11 +1503,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/braces": {
|
"node_modules/braces": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fill-range": "^7.0.1"
|
"fill-range": "^7.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@ -1859,9 +1868,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.0.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"to-regex-range": "^5.0.1"
|
"to-regex-range": "^5.0.1"
|
||||||
},
|
},
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
"type-check": "vue-tsc --build --force"
|
"type-check": "vue-tsc --build --force"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@catppuccin/tailwindcss": "^0.1.6",
|
||||||
"autoprefixer": "^10.4.18",
|
"autoprefixer": "^10.4.18",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
|
23
materia-web-client/src/materia-frontend/src/api/auth.ts
Normal file
23
materia-web-client/src/materia-frontend/src/api/auth.ts
Normal 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);
|
||||||
|
}
|
@ -13,12 +13,19 @@ export class HttpError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ResponseError {
|
export interface ResponseError {
|
||||||
status_code: number,
|
status: number | null,
|
||||||
message: string
|
message: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handle_error(error: AxiosError): Promise<ResponseError> {
|
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;
|
const debug = import.meta.hot;
|
||||||
|
@ -1 +1,2 @@
|
|||||||
|
export * as auth from "@/api/auth";
|
||||||
export * as user from "@/api/user";
|
export * as user from "@/api/user";
|
||||||
|
@ -4,24 +4,37 @@
|
|||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
body {
|
body {
|
||||||
background-color: rgba(40, 30, 30, 1); /*linear-gradient(rgba(36, 14, 84, 1) 80%, rgba(55, 22, 130, 1)); */
|
@apply bg-ctp-crust;
|
||||||
/*background-image: url("./background.svg");*/
|
font-family: Inter,sans-serif;
|
||||||
background-position: left top;
|
font-weight: 400;
|
||||||
background-repeat: repeat-x;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@apply text-green-500 hover:text-green-400;
|
@apply text-ctp-green;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
.input {
|
||||||
font-family: BioRhyme,serif;
|
@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;
|
||||||
font-weight: 700;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
font-family: Space Mono,monospace;
|
@apply text-ctp-text;
|
||||||
font-weight: 500;
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative h-12 border-b border-b-zinc-500">
|
<div class="relative h-12">
|
||||||
<nav
|
<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">
|
<div class="items-center m-0 flex">
|
||||||
<slot name="left"></slot>
|
<slot name="left"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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>
|
@ -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>
|
|
1
materia-web-client/src/materia-frontend/src/index.ts
Normal file
1
materia-web-client/src/materia-frontend/src/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * as api from "@/api";
|
@ -47,12 +47,12 @@ const router = createRouter({
|
|||||||
component: () => import("@/views/Home.vue"),
|
component: () => import("@/views/Home.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/user/login", name: "signin", beforeEnter: [bypass_auth],
|
path: "/auth/signin", name: "signin", beforeEnter: [bypass_auth],
|
||||||
component: () => import("@/views/user/SignIn.vue")
|
component: () => import("@/views/auth/SignIn.vue")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/user/register", name: "signup", //beforeEnter: [bypass_auth],
|
path: "/auth/signup", name: "signup", //beforeEnter: [bypass_auth],
|
||||||
component: () => import("@/views/user/SignUp.vue")
|
component: () => import("@/views/auth/SignUp.vue")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/user/preferencies", name: "prefs", redirect: { name: "prefs-profile" }, beforeEnter: [required_auth],
|
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],
|
path: "/:pathMatch(.*)*", name: "not-found", beforeEnter: [bypass_auth],
|
||||||
component: () => import("@/views/error/NotFound.vue")
|
component: () => import("@/views/NotFound.vue")
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
@ -26,7 +26,7 @@ async function signout() {
|
|||||||
<div class="flex-grow pb-20">
|
<div class="flex-grow pb-20">
|
||||||
<NavBar>
|
<NavBar>
|
||||||
<template #left>
|
<template #left>
|
||||||
<!-- TODO: logo -->
|
<RouterLink class="link-button" to="/">Home</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
<template #right>
|
<template #right>
|
||||||
<DropdownMenu v-if="userStore.current">
|
<DropdownMenu v-if="userStore.current">
|
||||||
@ -57,23 +57,23 @@ async function signout() {
|
|||||||
</template>
|
</template>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
<RouterLink v-if="!userStore.current"
|
<RouterLink v-if="!userStore.current" class="link-button" to="/user/login">Sign In</RouterLink>
|
||||||
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>
|
|
||||||
</template>
|
</template>
|
||||||
</NavBar>
|
</NavBar>
|
||||||
|
|
||||||
<main>
|
<main class="w-[1000px] ml-auto mr-auto pt-5 pb-5">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative overflow-hidden h-full ">
|
<div class="relative overflow-hidden h-full ">
|
||||||
</div>
|
</div>
|
||||||
<footer
|
<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">
|
class="flex justify-between pb-2 pt-2 pl-5 pr-5 bg-ctp-mantle">
|
||||||
<a href="/">Made with glove</a>
|
<a href="/">Made with glove by Elnafo, 2024</a>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
</div>
|
||||||
<a href="/api/docs">API</a>
|
<a href="/api/docs">API</a>
|
||||||
</footer>
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Base from '@/views/Base.vue';
|
import Base from "@/views/Base.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Base>
|
<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>
|
</Base>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Base from "@/views/Base.vue";
|
import Base from "@/views/Base.vue";
|
||||||
import Error from "@/components/error/Error.vue";
|
import Error from "@/components/Error.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
@ -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>
|
@ -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>
|
@ -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>
|
|
@ -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>
|
|
@ -21,7 +21,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [
|
||||||
|
require("@catppuccin/tailwindcss")({
|
||||||
|
prefix: "ctp",
|
||||||
|
defaultFlavour: "macchiato"
|
||||||
|
})
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user