Compare commits

..

12 Commits

Author SHA1 Message Date
e86d909e07 ah 2023-03-30 22:58:48 +02:00
39ba4856a2 wip 2023-02-05 22:06:48 +01:00
51a95f9bf1 WIP cards 2022-11-10 00:30:52 +01:00
1415da72df Update Request 2022-11-06 18:46:00 +01:00
0bb28ce89a update + schema validate 2022-11-06 18:40:30 +01:00
5c21712344 body length 2022-11-02 20:36:13 +01:00
82d356ef5e better errors + better tools 2022-10-30 19:35:35 +01:00
46a7e07b86 Windaude 2022-10-28 15:47:15 +02:00
fd335ce5cd added camera view behind 3D view 2022-10-28 15:24:39 +02:00
a4f5cf18d8 Init requests 2022-10-26 21:47:56 +02:00
e9e1b48970 insomnia folder 2022-10-26 21:43:05 +02:00
0f53063832 better errors 2022-10-26 00:16:41 +02:00
51 changed files with 3748 additions and 184 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@
**/config
!.prettierrc.json
!.insomnia

View File

@ -0,0 +1,8 @@
_id: spc_8436777c430547adb1085fd68e18eace
type: ApiSpec
parentId: wrk_01922deddc7342ddb4d070126530214b
modified: 1666813426216
created: 1666813426216
fileName: Rate
contents: ""
contentType: yaml

View File

@ -0,0 +1,14 @@
_id: env_25eaa5dc66424aeabb57ef52fb713da8
type: Environment
parentId: env_9b022c4edb1e6f6aa558cd2b00817d97d73ac78a
modified: 1666813483038
created: 1666813431942
name: local
data:
URL: http://localhost:8000
dataPropertyOrder:
"&":
- URL
color: null
isPrivate: false
metaSortKey: 1666813431942

View File

@ -0,0 +1,14 @@
_id: env_589a320c58d54efc8e9ab96f6506ebd0
type: Environment
parentId: env_9b022c4edb1e6f6aa558cd2b00817d97d73ac78a
modified: 1666813482405
created: 1666813453631
name: yayeet.cf
data:
URL: https://html5.yayeet.cf/api
dataPropertyOrder:
"&":
- URL
color: null
isPrivate: false
metaSortKey: 1666813453631

View File

@ -0,0 +1,11 @@
_id: env_9b022c4edb1e6f6aa558cd2b00817d97d73ac78a
type: Environment
parentId: wrk_01922deddc7342ddb4d070126530214b
modified: 1666813430220
created: 1666813426222
name: Base Environment
data: {}
dataPropertyOrder: {}
color: null
isPrivate: false
metaSortKey: 1666813426222

View File

@ -0,0 +1,21 @@
_id: req_7a775a1b9142457eb390ccd6b2e72f28
type: Request
parentId: fld_c78e326c656f4411b8899877dd4f3b9c
modified: 1666813549000
created: 1666813535408
url: "{{ _.URL }}/user/logout"
name: Logout
description: ""
method: POST
body: {}
parameters: []
headers: []
authentication: {}
metaSortKey: -1666813492452
isPrivate: false
settingStoreCookies: true
settingSendCookies: true
settingDisableRenderRequestBody: false
settingEncodeUrl: true
settingRebuildPath: true
settingFollowRedirects: global

View File

@ -0,0 +1,30 @@
_id: req_b0a828e3ee4d4267bd036b10d6bb1bed
type: Request
parentId: fld_c78e326c656f4411b8899877dd4f3b9c
modified: 1667756731637
created: 1666813566609
url: "{{ _.URL }}/user/create"
name: Create
description: ""
method: POST
body:
mimeType: application/json
text: |-
{
"username": "test2",
"password": "coucou",
"role": "USER"
}
parameters: []
headers:
- name: Content-Type
value: application/json
authentication: {}
metaSortKey: -1666813492352
isPrivate: false
settingStoreCookies: true
settingSendCookies: true
settingDisableRenderRequestBody: false
settingEncodeUrl: true
settingRebuildPath: true
settingFollowRedirects: global

View File

@ -0,0 +1,21 @@
_id: req_bf97a9e369584040ac3a8440c7b2bc7c
type: Request
parentId: fld_c78e326c656f4411b8899877dd4f3b9c
modified: 1666813598408
created: 1666813551357
url: "{{ _.URL }}/user/read/ff08bc79-ba76-432d-9709-34eefbc462eb"
name: Read
description: ""
method: GET
body: {}
parameters: []
headers: []
authentication: {}
metaSortKey: -1666813492402
isPrivate: false
settingStoreCookies: true
settingSendCookies: true
settingDisableRenderRequestBody: false
settingEncodeUrl: true
settingRebuildPath: true
settingFollowRedirects: global

View File

@ -0,0 +1,30 @@
_id: req_dbbe63eee56d439bb93f4c4929fcbfb2
type: Request
parentId: fld_c78e326c656f4411b8899877dd4f3b9c
modified: 1667756736645
created: 1666813492501
url: "{{ _.URL }}/user/login/"
name: Login
description: ""
method: POST
body:
mimeType: application/json
text: |-
{
"username": "admin",
"password": "motdepasse"
}
parameters: []
headers:
- name: Content-Type
value: application/json
id: pair_32919a49053545f5b23698f4c5724a56
authentication: {}
metaSortKey: -1666813492502
isPrivate: false
settingStoreCookies: true
settingSendCookies: true
settingDisableRenderRequestBody: false
settingEncodeUrl: true
settingRebuildPath: true
settingFollowRedirects: global

View File

@ -0,0 +1,28 @@
_id: req_f2d435a1ee5b47b1b7b6e2be9db8a8bb
type: Request
parentId: fld_c78e326c656f4411b8899877dd4f3b9c
modified: 1667756731065
created: 1667753525832
url: "{{ _.URL }}/user/update/ff08bc79-ba76-432d-9709-34eefbc462eb"
name: Update
description: ""
method: PATCH
body:
mimeType: application/json
text: |-
{
"username": "admin2"
}
parameters: []
headers:
- name: Content-Type
value: application/json
authentication: {}
metaSortKey: -1666813492302
isPrivate: false
settingStoreCookies: true
settingSendCookies: true
settingDisableRenderRequestBody: false
settingEncodeUrl: true
settingRebuildPath: true
settingFollowRedirects: global

View File

@ -0,0 +1,10 @@
_id: fld_c78e326c656f4411b8899877dd4f3b9c
type: RequestGroup
parentId: wrk_01922deddc7342ddb4d070126530214b
modified: 1666813489256
created: 1666813489256
name: User
description: ""
environment: {}
environmentPropertyOrder: null
metaSortKey: -1666813489256

View File

@ -0,0 +1,8 @@
_id: wrk_01922deddc7342ddb4d070126530214b
type: Workspace
parentId: null
modified: 1666813426216
created: 1666813426216
name: Rate
description: ""
scope: design

View File

@ -1,19 +1,21 @@
import './paths';
import Server from './framework/express/server';
import ip from 'ip';
import UserModel from './framework/mongo/user';
import { MongoClient } from 'mongodb';
import ip from 'ip';
import Server from './framework/express/server';
import UserModel from './framework/mongo/user';
import CardModel from './framework/mongo/card';
import { Config } from './config';
import { exit, env } from 'process';
export type Services = {
userModel: UserModel;
cardModel: CardModel;
};
const configFile = env.CONFIGFILE;
const configFile = process.env.CONFIGFILE;
if (configFile === undefined) {
console.log('env var CONFIGFILE not set');
exit(1);
process.exit(1);
}
const config = new Config(configFile);
@ -23,14 +25,22 @@ const db = mongo.db(config.mongo.dbName);
const services: Services = {
userModel: new UserModel(db),
cardModel: new CardModel(db),
};
const server = new Server(config.server, config.mongo, services);
server.start(() =>
process.on('SIGINT', async () => {
console.log('\nClosing Api...');
await mongo.close();
await server.close();
process.exit();
});
server.start().then(() => {
console.log(
`Running on http://127.0.0.1:${config.server.port} http://${ip.address()}:${
config.server.port
}`,
),
);
});

View File

@ -16,6 +16,15 @@ export class Config {
constructor(configFile: string) {
const config = JSON.parse(readFileSync(configFile).toString()) as Config;
if (
config.mongo.dbName === undefined ||
config.mongo.uri === undefined ||
config.server.origin === undefined ||
config.server.port === undefined
) {
console.log('Error in Config File');
process.exit(1);
}
this.server = config.server;
this.mongo = config.mongo;
}

37
api/src/entities/card.ts Normal file
View File

@ -0,0 +1,37 @@
import { CardCtor, CardInfo, CardRarity, CombatStats } from '@core';
import { Entity } from './entity';
export class Card extends Entity implements CardInfo {
name: string;
rarity: keyof typeof CardRarity;
stats: CombatStats;
thumbnail: string;
model: string;
constructor(raw: CardCtor) {
super(raw);
this.name = raw.name || '';
this.rarity = raw.rarity || CardRarity.COMMON;
this.stats = raw.stats || {
baseAtk: 0,
baseCrit: 0,
baseDef: 0,
multAtk: 0,
multCrit: 0,
multDef: 0,
};
this.thumbnail = raw.thumbnail || '';
this.model = raw.model || '';
}
Info(): CardInfo {
return {
uuid: this.uuid,
name: this.name,
rarity: this.rarity,
stats: this.stats,
thumbnail: this.thumbnail,
model: this.model,
};
}
}

View File

@ -1,12 +1,15 @@
import { EntityCtor, EntityInfo } from '@core';
import { randomUUID } from 'crypto';
import { ObjectId } from 'mongodb';
export class Entity implements EntityInfo {
_id: ObjectId;
uuid: string;
createdAt: Date;
updatedAt: Date;
constructor(raw: EntityCtor) {
this._id = raw._id || new ObjectId();
this.uuid = raw.uuid || randomUUID();
this.createdAt = raw.createdAt || new Date();
this.updatedAt = raw.updatedAt || new Date();

View File

@ -1,22 +1,26 @@
import {
CardInfo,
UserCtor,
UserInfo,
UserInfoWithPassword,
UserRoles,
UserWithPasswordCtor,
} from '@core';
import { generateHash } from '../functions/password';
import { getHashedPassword } from '../functions/password';
import { Card } from './card';
import { Entity } from './entity';
export class User extends Entity implements UserInfo {
username: string;
role: keyof typeof UserRoles;
cards: Card[];
constructor(raw: UserCtor) {
super(raw);
this.username = raw.username || '';
this.role = raw.role || UserRoles.USER;
this.cards = raw.cards ? raw.cards.map(card => new Card(card)) : [];
}
Info(): UserInfo {
@ -24,6 +28,7 @@ export class User extends Entity implements UserInfo {
uuid: this.uuid,
username: this.username,
role: this.role,
cards: this.cards.map(card => card.Info()),
};
}
}
@ -34,7 +39,7 @@ export class UserWithPassword extends User implements UserInfoWithPassword {
constructor(raw: UserWithPasswordCtor) {
super(raw);
this.password = generateHash(this.uuid, raw.password || '');
this.password = getHashedPassword(this.uuid, raw.password || '');
}
Info(): UserInfoWithPassword {
@ -42,6 +47,7 @@ export class UserWithPassword extends User implements UserInfoWithPassword {
uuid: this.uuid,
username: this.username,
role: this.role,
cards: this.cards.map(card => card.Info()),
password: this.password,
};
}

View File

@ -0,0 +1,66 @@
import { RequestHandler, Router } from 'express';
import { CreateCard, ListCards, ReadCard } from '../../functions/card';
import { Services } from '../../app';
import { CheckPermissions, getRequestId, ValidateSchema } from './middleware';
import { CreateCardSchema, ReadCardSchema } from './schema/card';
function CreateHandler(services: Services): RequestHandler {
const createCard = CreateCard(services);
return async (req, res, next) => {
try {
const card = await createCard(getRequestId(req), req.body);
res.status(200).send(card);
} catch (error) {
next({ status: 409, message: 'Card Already Exists' });
}
};
}
function ReadHandler(services: Services): RequestHandler {
const readCard = ReadCard(services);
return async (req, res, next) => {
try {
const card = await readCard(getRequestId(req), req.params.uuid);
res.status(200).send(card);
} catch (error) {
next({ status: 404, message: 'Card Not Found' });
}
};
}
function ListHandler(services: Services): RequestHandler {
const listCards = ListCards(services);
return async (req, res, next) => {
try {
const cards = await listCards(getRequestId(req));
res.status(200).send(cards);
} catch (error) {
next({ message: 'Unknown Error' });
}
};
}
export function Routes(services: Services) {
const router = Router();
router.post(
'/create',
CheckPermissions(),
CreateCardSchema(),
ValidateSchema(),
CreateHandler(services),
);
router.get(
'/read/:uuid',
CheckPermissions(),
ReadCardSchema(),
ValidateSchema(),
ReadHandler(services),
);
router.get('/list', CheckPermissions(), ListHandler(services));
return router;
}

View File

@ -1,6 +1,6 @@
import { ErrorRequestHandler, Request, RequestHandler } from 'express';
import { randomUUID } from 'crypto';
import { validationResult } from 'express-validator';
import { validationResult, matchedData } from 'express-validator';
import { UserInfo, UserRoles } from '@core';
declare module 'express-session' {
@ -9,22 +9,22 @@ declare module 'express-session' {
}
}
export function getId(req: Request): string {
return req.header('request-id') || 'unknown';
export function getRequestId(req: Request): string {
return req.header('x-request-id') || 'unknown';
}
export function RequestId(): RequestHandler {
return (req, res, next) => {
req.headers['request-id'] = randomUUID();
req.headers['x-request-id'] = randomUUID();
next();
};
}
export function CheckPermissions(): RequestHandler {
function getResourceId(req: Request): string | null {
function getResourceId(req: Request): string | undefined {
if (req.params.uuid) return req.params.uuid;
if (req.body.uuid) return req.body.uuid;
return null;
return undefined;
}
function canAccessRessource(user: UserInfo, uuid: string): boolean {
@ -34,34 +34,37 @@ export function CheckPermissions(): RequestHandler {
return (req, res, next) => {
if (!req.session.user) {
next({ status: 401, messsage: 'Unauthorized' });
return;
return next({ status: 401, messsage: 'Unauthorized' });
}
if (req.session.user.role === UserRoles.ADMIN) {
next();
return;
return next();
}
const ressourceId = getResourceId(req);
if (!ressourceId) {
next({ status: 403, messsage: 'Forbidden' });
return;
}
if (canAccessRessource(req.session.user, ressourceId)) {
next();
return;
} else {
next({ status: 403, messsage: 'Forbidden' });
return;
return next({ status: 403, messsage: 'Forbidden' });
}
next({ status: 401, messsage: 'Unauthorized' });
if (canAccessRessource(req.session.user, ressourceId)) {
return next();
}
next({ status: 403, messsage: 'Forbidden' });
};
}
export function SchemaValidator(): RequestHandler {
export function ValidateSchema(): RequestHandler {
return (req, res, next) => {
const oldBody = req.body;
req.body = matchedData(req, { locations: ['body'] });
if (JSON.stringify(oldBody) !== JSON.stringify(req.body))
return next({
status: 422,
message: 'Unprocessable Entity',
});
const error = validationResult(req);
error.isEmpty()
? next()
@ -72,10 +75,16 @@ export function SchemaValidator(): RequestHandler {
};
}
export function NotFoundHandler(): RequestHandler {
return (req, res, next) => {
next({ status: 404, message: 'Not Found' });
};
}
export function ErrorHandler(): ErrorRequestHandler {
return (error, req, res, next) => {
error.status
? res.status(error.status).send(error)
: res.status(500).send(error);
: res.status(500).send({ status: 500, message: error.message });
};
}

View File

@ -1,11 +1,13 @@
import { Router } from 'express';
import { Services } from '../../app';
import * as user from './user';
import * as card from './card';
export function Routes(services: Services) {
const router = Router();
router.use('/user', user.Routes(services));
router.use('/card', card.Routes(services));
return router;
}

View File

@ -0,0 +1,29 @@
import { checkSchema } from 'express-validator';
import { CardRarity } from '@core';
export const CreateCardSchema = () =>
checkSchema({
name: {
isString: true,
},
rarity: {
isIn: {
options: [Object.values(CardRarity)],
},
},
thumbnail: {
isString: true,
},
model: {
isString: true,
},
});
export const ReadCardSchema = () =>
checkSchema({
uuid: {
isUUID: {
options: 4,
},
},
});

View File

@ -1,15 +1,6 @@
import { checkSchema } from 'express-validator';
import { UserRoles } from '@core';
export const ReadUserSchema = () =>
checkSchema({
uuid: {
isUUID: {
options: 4,
},
},
});
export const LoginUserSchema = () =>
checkSchema({
username: {
@ -36,3 +27,65 @@ export const CreateUserSchema = () =>
},
},
});
export const ReadUserSchema = () =>
checkSchema({
uuid: {
isUUID: {
options: 4,
},
},
});
export const ListUsersSchema = () => checkSchema({});
export const UpdateUserSchema = () =>
checkSchema({
uuid: {
isUUID: {
options: 4,
},
},
username: {
isString: true,
optional: true,
},
password: {
isString: true,
optional: true,
},
role: {
isIn: {
options: [Object.values(UserRoles)],
},
optional: true,
},
});
export const AddCardToUserSchema = () =>
checkSchema({
uuid: {
isUUID: {
options: 4,
},
},
cardUuid: {
isUUID: {
options: 4,
},
},
});
export const RemoveCardFromUserSchema = () =>
checkSchema({
uuid: {
isUUID: {
options: 4,
},
},
cardUuid: {
isUUID: {
options: 4,
},
},
});

View File

@ -1,15 +1,18 @@
import express, { Express } from 'express';
import cors from 'cors';
import session from 'express-session';
import MongoStore from 'connect-mongo';
import { randomUUID } from 'crypto';
import * as http from 'http';
import { Routes } from './router';
import { RequestId, ErrorHandler } from './middleware';
import { RequestId, ErrorHandler, NotFoundHandler } from './middleware';
import { Services } from '../../app';
import { MongoConfig, ServerConfig } from '../../config';
import { randomUUID } from 'crypto';
import MongoStore from 'connect-mongo';
class Server {
private app: Express;
private server: http.Server;
private config: ServerConfig;
constructor(
@ -20,6 +23,7 @@ class Server {
this.config = config;
this.app = express();
this.app.disable('x-powered-by');
this.app.use(express.json());
this.app.use(
cors({
@ -28,7 +32,6 @@ class Server {
origin: this.config.origin,
}),
);
this.app.use(RequestId());
this.app.use(
session({
proxy: process.env.NODE_ENV === 'production',
@ -48,12 +51,25 @@ class Server {
saveUninitialized: false,
}),
);
this.app.use(RequestId());
this.app.use(Routes(services));
this.app.use(NotFoundHandler());
this.app.use(ErrorHandler());
}
start(func: () => void): void {
this.app.listen(this.config.port, func);
async start(): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.server = this.app.listen(this.config.port, resolve);
});
}
async close(): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.server.close((error) => {
if (error) reject(error);
resolve();
});
});
}
}

View File

@ -1,86 +1,182 @@
import { RequestHandler, Router } from 'express';
import { Services } from '../../app';
import { CreateUser, ReadUser, LoginUser } from '../../functions/user';
import {
CreateUser,
ReadUser,
LoginUser,
UpdateUser,
ListUsers,
AddCardToUser,
} from '../../functions/user';
import {
LoginUserSchema,
CreateUserSchema,
ReadUserSchema,
LogoutUserSchema,
UpdateUserSchema,
ListUsersSchema,
AddCardToUserSchema,
RemoveCardFromUserSchema,
} from './schema/user';
import { CheckPermissions, getId, SchemaValidator } from './middleware';
import { CheckPermissions, getRequestId, ValidateSchema } from './middleware';
function LoginHandler(services: Services): RequestHandler {
function LoginUserHandler(services: Services): RequestHandler {
const login = LoginUser(services);
return async (req, res, next) => {
const user = await login(getId(req), req.body);
try {
const user = await login(getRequestId(req), req.body);
user ? (req.session.user = user) : (req.session.user = null);
user
? res.status(200).send(user)
: next({ status: 401, message: 'wrong username or password' });
res.status(200).send(user);
} catch (error) {
next({ status: 401, message: 'wrong username or password' });
}
};
}
function LogoutHandler(services: Services): RequestHandler {
function LogoutUserHandler(services: Services): RequestHandler {
return async (req, res, next) => {
if (req.session.user) {
req.session.user = null;
res.status(204).send();
} else {
next({ status: 401, message: 'not logged in' });
next({ status: 401, message: 'Not Logged In' });
}
};
}
function CreateHandler(services: Services): RequestHandler {
function CreateUserHandler(services: Services): RequestHandler {
const createUser = CreateUser(services);
return async (req, res, next) => {
const user = await createUser(getId(req), req.body);
user ? res.status(201).send(user) : next();
try {
const user = await createUser(getRequestId(req), req.body);
res.status(201).send(user);
} catch (error) {
next({ status: 409, message: 'User Already Exists' });
}
};
}
function ReadHandler(services: Services): RequestHandler {
function ReadUserHandler(services: Services): RequestHandler {
const readUser = ReadUser(services);
return async (req, res, next) => {
const user = await readUser(getId(req), req.params.uuid);
user
? res.status(200).send(user)
: next({ status: 404, message: 'user not found' });
try {
const user = await readUser(getRequestId(req), req.params.uuid);
res.status(200).send(user);
} catch (error) {
next({ status: 404, message: 'User Not Found' });
}
};
}
function UpdateUserHandler(services: Services): RequestHandler {
const updateUser = UpdateUser(services);
return async (req, res, next) => {
try {
const user = await updateUser(
getRequestId(req),
req.params.uuid,
req.body,
);
res.status(200).send(user);
} catch (error) {
next({ status: 404, message: 'User Not Found' });
}
};
}
function ListUserHandler(services: Services): RequestHandler {
const listUsers = ListUsers(services);
return async (req, res, next) => {
try {
const users = await listUsers(getRequestId(req));
res.status(200).send(users);
} catch (error) {
next({ message: 'Unknown Error' });
}
};
}
function AddCardToUserHandler(services: Services): RequestHandler {
const addCardToUser = AddCardToUser(services);
return async (req, res, next) => {
try {
const user = await addCardToUser(
getRequestId(req),
req.params.uuid,
req.body.cardUuid,
);
res.status(200).send(user);
} catch (error) {
next({ message: 'Unknown Error' });
}
};
}
export function Routes(services: Services) {
const router = Router();
//User
router.post(
'/login',
LoginUserSchema(),
SchemaValidator(),
LoginHandler(services),
ValidateSchema(),
LoginUserHandler(services),
);
router.post(
'/logout',
LogoutUserSchema(),
SchemaValidator(),
LogoutHandler(services),
ValidateSchema(),
LogoutUserHandler(services),
);
router.get(
'/read/:uuid',
ReadUserSchema(),
SchemaValidator(),
CheckPermissions(),
ReadHandler(services),
ReadUserSchema(),
ValidateSchema(),
ReadUserHandler(services),
);
router.post(
'/create',
CreateUserSchema(),
SchemaValidator(),
CheckPermissions(),
CreateHandler(services),
CreateUserSchema(),
ValidateSchema(),
CreateUserHandler(services),
);
router.patch(
'/update/:uuid',
CheckPermissions(),
UpdateUserSchema(),
ValidateSchema(),
UpdateUserHandler(services),
);
router.get(
'/list',
CheckPermissions(),
ListUsersSchema(),
ValidateSchema(),
ListUserHandler(services),
);
//Alteration
router.post(
'/addCard/:uuid',
CheckPermissions(),
AddCardToUserSchema(),
ValidateSchema(),
AddCardToUserHandler(services),
);
router.post(
'/removeCard/:uuid',
CheckPermissions(),
RemoveCardFromUserSchema(),
ValidateSchema(),
//RemoveCardFromUserHandler(),
);
return router;

View File

@ -0,0 +1,74 @@
import { Collection, Db } from 'mongodb';
import { UpdateCardBody } from '@core';
import { Card } from '../../entities/card';
import { log } from '../../functions/logger';
class CardModel {
private collection: Collection<Card>;
constructor(db: Db) {
this.collection = db.collection<Card>('cards');
}
public async create(tracker: string, cardInfo: Card): Promise<Card> {
const checkCard = await this.collection.findOne({
uuid: cardInfo.uuid,
});
if (checkCard) {
log(tracker, 'Card Already Exists');
throw new Error();
}
await this.collection.insertOne(cardInfo);
const card = await this.read(tracker, cardInfo.uuid);
log(tracker, 'CREATE USER', card);
return card;
}
public async read(tracker: string, uuid: string): Promise<Card> {
const cardDocument = await this.collection.findOne({ uuid });
if (!cardDocument) {
log(tracker, 'Card Not Found');
throw new Error();
}
const card = new Card(cardDocument);
log(tracker, 'READ CARD', card);
return card;
}
public async update(
tracker: string,
uuid: string,
cardInfo: UpdateCardBody,
): Promise<Card> {
const checkCard = await this.collection.findOne({ uuid });
if (!checkCard) {
log(tracker, 'Card Does Not Exist');
throw new Error();
}
await this.collection.updateOne(
{ uuid },
{
$set: {
...cardInfo,
updatedAt: new Date(),
},
},
);
const card = await this.read(tracker, uuid);
log(tracker, 'UPDATE CARD', card);
return card;
}
public async list(tracker: string): Promise<Card[]> {
const cardCollection = await this.collection.find().toArray();
log(tracker, 'LIST CARDS');
return cardCollection.map((cardDocument) => new Card(cardDocument));
}
}
export default CardModel;

View File

@ -1,8 +1,8 @@
import { Collection, Db } from 'mongodb';
import { LoginUserBody } from '../../../../core/src/user';
import { Card, CardInfo, LoginUserBody, UpdateUserBody, UserInfo } from '@core';
import { User, UserWithPassword } from '../../entities/user';
import log from '../../functions/logger';
import { generateHash } from '../../functions/password';
import { log } from '../../functions/logger';
import { getHashedPassword } from '../../functions/password';
class UserModel {
private collection: Collection<User>;
@ -11,42 +11,136 @@ class UserModel {
this.collection = db.collection<User>('users');
}
public async login(tracker: string, data: LoginUserBody) {
public async login(tracker: string, userInfo: LoginUserBody): Promise<User> {
const checkUser = await this.collection.findOne({
username: data.username,
username: userInfo.username,
});
if (checkUser === null) {
if (!checkUser) {
log(tracker, 'User Not Found');
return null;
throw new Error();
}
const user = await this.collection.findOne({
const userDocument = await this.collection.findOne({
uuid: checkUser.uuid,
password: generateHash(checkUser.uuid, data.password),
password: getHashedPassword(checkUser.uuid, userInfo.password),
});
if (user === null) {
if (!userDocument) {
log(tracker, 'Wrong Password');
return null;
throw new Error();
}
const user = new User(userDocument);
log(tracker, 'LOG IN', user);
return new User(user);
return user;
}
public async create(tracker: string, user: UserWithPassword) {
await this.collection.insertOne(user);
public async create(
tracker: string,
userInfo: UserWithPassword,
): Promise<User> {
const checkUser = await this.collection.findOne({
username: userInfo.username,
});
if (checkUser) {
log(tracker, 'User Already Exists');
throw new Error();
}
await this.collection.insertOne(userInfo);
const user = await this.read(tracker, userInfo.uuid);
log(tracker, 'CREATE USER', user);
return this.read(tracker, user.uuid);
return user;
}
public async read(tracker: string, uuid: string) {
const user = await this.collection.findOne({ uuid });
if (user === null) {
log(tracker, 'User Not Found');
return null;
public async read(tracker: string, uuid: string): Promise<User> {
const [userDocument] = await this.collection.aggregate<UserInfo>([
{
$match:{
uuid
}
},
{
$lookup: {
from: "cards",
localField: "cards",
foreignField: "_id",
as: "cards"
}
}
]).toArray();
console.log(JSON.stringify(userDocument));
if (!userDocument) {
log(tracker, 'User Not Found');
throw new Error();
}
const user = new User(userDocument);
log(tracker, 'READ USER', user);
return new User(user);
return user;
}
public async update(
tracker: string,
uuid: string,
userInfo: UpdateUserBody,
): Promise<User> {
const checkUser = await this.collection.findOne({ uuid });
if (!checkUser) {
log(tracker, 'User Does Not Exist');
throw new Error();
}
if (userInfo.password)
userInfo.password = getHashedPassword(uuid, userInfo.password);
await this.collection.updateOne(
{ uuid },
{
$set: {
...userInfo,
updatedAt: new Date(),
},
},
);
const user = await this.read(tracker, uuid);
log(tracker, 'UPDATE USER', user);
return user;
}
public async list(tracker: string): Promise<User[]> {
const userCollection = await this.collection.find().toArray();
log(tracker, 'LIST USERS');
return userCollection.map((userDocument) => new User(userDocument));
}
public async addCard(
tracker: string,
uuid: string,
card: Card,
): Promise<User> {
const checkUser = await this.collection.findOne({ uuid });
if (!checkUser) {
log(tracker, 'User Does Not Exist');
throw new Error();
}
await this.collection.updateOne(
{ uuid },
{
$set: {
updatedAt: new Date(),
},
$addToSet: {
cards: card._id,
},
},
);
const user = await this.read(tracker, uuid);
log(tracker, 'ADD CARD TO USER', user, card);
return user;
}
}

36
api/src/functions/card.ts Normal file
View File

@ -0,0 +1,36 @@
import { CardInfo, CreateCardBody } from '@core';
import { Card } from '../entities/card';
import { Services } from '../app';
export function CreateCard(
services: Services,
): (tracker: string, raw: CreateCardBody) => Promise<CardInfo> {
const { cardModel } = services;
return async (tracker, raw) => {
const card = await cardModel.create(tracker, new Card(raw));
return card.Info();
};
}
export function ReadCard(
services: Services,
): (tracker: string, uuid: string) => Promise<CardInfo> {
const { cardModel } = services;
return async (tracker, uuid) => {
const card = await cardModel.read(tracker, uuid);
return card.Info();
};
}
export function ListCards(
services: Services,
): (tracker: string) => Promise<CardInfo[]> {
const { cardModel } = services;
return async (tracker) => {
const cards = await cardModel.list(tracker);
return cards.map((card) => card.Info());
};
}

View File

@ -1,11 +1,10 @@
export function log(tracker: string, ...message: unknown[]) {
if (message)
message.forEach((obj, index) => {
try {
message[index] = JSON.stringify(obj);
} catch {}
});
tracker ? console.log(`[${tracker}]`, ...message) : console.log(...message);
message ? console.log(`[${tracker}]`, ...message) : console.log(tracker);
}
export default log;

View File

@ -1,5 +1,9 @@
import { createHash } from 'crypto';
export function getHashedPassword(uuid: string, password: string) {
return generateHash(uuid, password);
}
export function generateHash(...values: string[]) {
return createHash('sha256').update(values.join('')).digest('hex');
}

View File

@ -1,36 +1,71 @@
import { CreateUserBody, LoginUserBody, UserInfo } from '@core';
import { CreateUserBody, LoginUserBody, UserInfo, UpdateUserBody } from '@core';
import { Services } from '../app';
import { UserWithPassword } from '../entities/user';
export function LoginUser(
services: Services,
): (tracker: string, raw: LoginUserBody) => Promise<UserInfo | null> {
): (tracker: string, raw: LoginUserBody) => Promise<UserInfo> {
const { userModel } = services;
return async (tracker, raw) => {
const user = await userModel.login(tracker, raw);
return user ? user.Info() : null;
return user.Info();
};
}
export function CreateUser(
services: Services,
): (tracker: string, raw: CreateUserBody) => Promise<UserInfo | null> {
): (tracker: string, raw: CreateUserBody) => Promise<UserInfo> {
const { userModel } = services;
return async (tracker, raw) => {
const user = await userModel.create(tracker, new UserWithPassword(raw));
return user ? user.Info() : null;
return user.Info();
};
}
export function ReadUser(
services: Services,
): (tracker: string, uuid: string) => Promise<UserInfo | null> {
): (tracker: string, uuid: string) => Promise<UserInfo> {
const { userModel } = services;
return async (tracker, uuid) => {
const user = await userModel.read(tracker, uuid);
return user ? user.Info() : null;
return user.Info();
};
}
export function UpdateUser(
services: Services,
): (tracker: string, uuid: string, raw: UpdateUserBody) => Promise<UserInfo> {
const { userModel } = services;
return async (tracker, uuid, raw) => {
const user = await userModel.update(tracker, uuid, raw);
return user.Info();
};
}
export function ListUsers(
services: Services,
): (tracker: string) => Promise<UserInfo[]> {
const { userModel } = services;
return async (tracker) => {
const users = await userModel.list(tracker);
return users.map((user) => user.Info());
};
}
export function AddCardToUser(
services: Services,
): (tracker: string, uuid: string, cardUuid: string) => Promise<UserInfo> {
const { userModel, cardModel } = services;
return async (tracker, uuid, cardUuid) => {
const card = await cardModel.read(tracker, cardUuid);
const user = await userModel.addCard(tracker, uuid, card);
return user.Info();
};
}

2568
core/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@
"devDependencies": {
"@types/node": "^18.8.4",
"prettier": "^2.7.1",
"mongodb": "^4.10.0",
"typescript": "^4.8.3"
}
}

12
core/src/alteration.ts Normal file
View File

@ -0,0 +1,12 @@
import { EntityInfo } from './entity';
import { CardInfo } from './card';
export type AlerationInfo = {
card: CardInfo;
level: number;
xp: number;
};
export type Ateration = AlerationInfo & EntityInfo;
export type AlerationCtor = Partial<Ateration>;

40
core/src/card.ts Normal file
View File

@ -0,0 +1,40 @@
import { EntityInfo } from './entity';
export enum CardRarity {
COMMON = 'COMMON',
RARE = 'RARE',
EPIC = 'EPIC',
LEGENDARY = 'LEGENDARY',
}
export type CombatStats = {
baseAtk: number;
baseDef: number;
baseCrit: number;
multAtk: number;
multDef: number;
multCrit: number;
};
export type CardInfo = {
uuid: string;
name: string;
rarity: keyof typeof CardRarity;
stats: CombatStats;
thumbnail: string;
model: string;
};
export type CreateCardBody = {
name: string;
rarity: keyof typeof CardRarity;
stats: CombatStats;
thumbnail: string;
model: string;
};
export type Card = CardInfo & EntityInfo;
export type CardCtor = Partial<Card>;
export type UpdateCardBody = Partial<Omit<CardInfo, 'uuid'>>;

View File

@ -1,4 +1,7 @@
import { ObjectId } from 'mongodb';
export type EntityInfo = {
_id: ObjectId;
uuid: string;
createdAt: Date;
updatedAt: Date;

View File

@ -1,2 +1,4 @@
export * from './alteration';
export * from './card';
export * from './user';
export * from './entity';

View File

@ -1,4 +1,5 @@
import { EntityInfo } from './entity';
import { CardInfo } from './card';
export enum UserRoles {
ADMIN = 'ADMIN',
@ -9,6 +10,7 @@ export type UserInfo = {
uuid: string;
username: string;
role: keyof typeof UserRoles;
cards: CardInfo[];
};
export type User = UserInfo & EntityInfo;
@ -29,6 +31,8 @@ export type CreateUserBody = {
role: keyof typeof UserRoles;
};
export type UpdateUserBody = Partial<Omit<UserInfoWithPassword, 'uuid'>>;
export type LoginUserBody = {
username: string;
password: string;

0
scripts/deploy.sh Executable file → Normal file
View File

26
scripts/tools.py Executable file → Normal file
View File

@ -1,6 +1,8 @@
#!/usr/bin/python
import os
import sys
from os import chdir, path
from sys import argv
from subprocess import run, PIPE
from multiprocessing import Process
apps = ['core', 'api', 'www']
commands = {
@ -14,19 +16,29 @@ def print_commands():
print('Available commands:', [c for c in commands])
def run_command_in_app(app: str, command: str):
chdir(app)
result = run(command.split(' '), stdout=PIPE, stderr=PIPE, text=True)
status = 'DONE' if result.returncode == 0 else 'ERROR'
print('%s:\t%s' % (app, status))
if status == 'ERROR':
print(result.stdout, result.stderr)
if __name__ == '__main__':
if len(sys.argv) < 2:
if len(argv) < 2:
print_commands()
exit()
cmd = sys.argv[1]
cmd = argv[1]
if cmd not in commands:
print('Command \'%s\' not available' % cmd)
print_commands()
exit()
chdir(path.join(path.dirname(path.realpath(__file__)), '..'))
print('Running \'%s\' on %d apps: %s' % (commands[cmd], len(apps), apps))
for app in apps:
os.chdir(app)
os.system(commands[cmd])
os.chdir('..')
Process(target=run_command_in_app, args=(app, commands[cmd])).start()

31
www/package-lock.json generated
View File

@ -22,6 +22,7 @@
"@rollup/plugin-typescript": "^8.0.0",
"@tsconfig/svelte": "^2.0.0",
"@types/three": "^0.144.0",
"@types/w3c-image-capture": "^1.0.7",
"prettier": "^2.7.1",
"prettier-plugin-svelte": "^2.8.0",
"rollup": "^2.3.4",
@ -353,6 +354,21 @@
"@types/webxr": "*"
}
},
"node_modules/@types/w3c-image-capture": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/w3c-image-capture/-/w3c-image-capture-1.0.7.tgz",
"integrity": "sha512-BNmjJT+yjwVzB6N5kf1/m9YZMF//e3JAcII+TIuhRkALn7UD9xZX0R1azVGqqyYuQKbQ6UskC+zSQctnsk9zHg==",
"dev": true,
"dependencies": {
"@types/webrtc": "*"
}
},
"node_modules/@types/webrtc": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/@types/webrtc/-/webrtc-0.0.33.tgz",
"integrity": "sha512-xjN6BelzkY3lzXjIjXGqJVDS6XDleEsvp1bVIyNccXCcMoTH3wvUXFew4/qflwJdNqjmq98Zc5VcALV+XBKBvg==",
"dev": true
},
"node_modules/@types/webxr": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.0.tgz",
@ -1986,6 +2002,21 @@
"@types/webxr": "*"
}
},
"@types/w3c-image-capture": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/w3c-image-capture/-/w3c-image-capture-1.0.7.tgz",
"integrity": "sha512-BNmjJT+yjwVzB6N5kf1/m9YZMF//e3JAcII+TIuhRkALn7UD9xZX0R1azVGqqyYuQKbQ6UskC+zSQctnsk9zHg==",
"dev": true,
"requires": {
"@types/webrtc": "*"
}
},
"@types/webrtc": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/@types/webrtc/-/webrtc-0.0.33.tgz",
"integrity": "sha512-xjN6BelzkY3lzXjIjXGqJVDS6XDleEsvp1bVIyNccXCcMoTH3wvUXFew4/qflwJdNqjmq98Zc5VcALV+XBKBvg==",
"dev": true
},
"@types/webxr": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.0.tgz",

View File

@ -5,7 +5,7 @@
"author": "Yanis Rigaudeau - Axel Barault",
"private": true,
"scripts": {
"build": "rollup -c",
"build": "rollup -c --failAfterWarnings",
"dev": "rollup -c -w",
"start": "sirv public --no-clear --host --single",
"check": "svelte-check --tsconfig ./tsconfig.json",
@ -18,6 +18,7 @@
"@rollup/plugin-typescript": "^8.0.0",
"@tsconfig/svelte": "^2.0.0",
"@types/three": "^0.144.0",
"@types/w3c-image-capture": "^1.0.7",
"prettier": "^2.7.1",
"prettier-plugin-svelte": "^2.8.0",
"rollup": "^2.3.4",

View File

@ -1,15 +1,29 @@
<script>
<script lang="ts">
import 'papercss/dist/paper.min.css';
import { onMount } from 'svelte';
import { Router, Route } from 'svelte-navigator';
import Test3D from './pages/test3D.svelte';
import { currentUser } from './store/user';
import { read } from './functions/user';
import ViewAR from './pages/ViewAR.svelte';
import Login from './pages/Login.svelte';
import Profile from './pages/Profile.svelte';
onMount(async () => {
if ($currentUser) {
try {
const user = await read($currentUser.uuid);
currentUser.set(user);
} catch (error) {
currentUser.set(null);
window.location.href = '/';
}
}
});
</script>
<Router>
<Route path="/">
<Test3D />
<ViewAR />
</Route>
<Route path="/login">
<Login />

View File

@ -1,16 +1,13 @@
<script lang="ts">
import NavBar from '../components/NavBar.svelte';
import { beforeUpdate, afterUpdate, onMount, onDestroy } from 'svelte';
import * as Three from 'three';
let container: HTMLElement;
let canvas: HTMLCanvasElement;
let width: number;
let height: number;
export let width: number;
export let height: number;
//#region variables
//let innerWidth: number = 0;
//let innerHeight: number = 0;
let container: HTMLElement;
let canvas: HTMLCanvasElement;
let maxInnerWidth: number = 0;
let maxInnerHeight: number = 0;
@ -18,6 +15,21 @@
let focalLength: number = 540 / Math.atan((70 * Math.PI) / 180);
let fov = 75;
//let baseHorizontalFOV: number = 2.0*Math.atan(baseAspectRation*Math.tan(baseVerticalFOV*0.5));
let scene: Three.Scene = new Three.Scene();
let camera: Three.PerspectiveCamera = new Three.PerspectiveCamera(
75,
1,
0.1,
1000,
);
let renderer: Three.WebGLRenderer = new Three.WebGLRenderer();
let box: Three.BoxGeometry = new Three.BoxGeometry(1, 1, 1);
let mat: Three.MeshBasicMaterial = new Three.MeshBasicMaterial({
color: 0x00ff00,
});
let cube = new Three.Mesh(box, mat);
//#endregion
//#region functions
@ -50,22 +62,6 @@
resetMaxDimensions();
}
}
//#endregion
let scene: Three.Scene = new Three.Scene();
let camera: Three.PerspectiveCamera = new Three.PerspectiveCamera(
75,
1,
0.1,
1000,
);
let renderer: Three.WebGLRenderer = new Three.WebGLRenderer();
let box: Three.BoxGeometry = new Three.BoxGeometry(1, 1, 1);
let mat: Three.MeshBasicMaterial = new Three.MeshBasicMaterial({
color: 0x00ff00,
});
let cube = new Three.Mesh(box, mat);
function animate() {
requestAnimationFrame(animate);
@ -75,6 +71,7 @@
renderer.render(scene, camera);
}
//#endregion
onMount(() => {
canvas = container.appendChild(renderer.domElement);
@ -96,7 +93,7 @@
camera.setViewOffset(width, height, 0, 0, width, height);
renderer.setSize(width, height);
//renderer.setClearColor(0x000000, (innerHeight/(maxInnerHeight))/2 + 0.5);
renderer.setClearColor(0x000000, 0.1);
});
afterUpdate(() => {});
@ -106,17 +103,15 @@
});
</script>
<div
id="container"
bind:this={container}
bind:clientWidth={width}
bind:clientHeight={height}
/>
<NavBar />
<div class="Container" bind:this={container} />
<!--<svelte:window bind:innerWidth bind:innerHeight /> -->
<style>
#container {
.Container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
</style>

View File

@ -0,0 +1,45 @@
<script lang="ts">
import { onMount } from 'svelte';
import CameraPreview from './CameraPreview.svelte';
import { getMediaStream, getImageCapture } from '../helpers/mediaHelper';
export let width: number;
export let height: number;
let container: HTMLElement;
let cameraWidth: number = 0;
let cameraHeight: number = 0;
let pictureUrl: string;
let imageCapture: ImageCapture;
let mediaStreamPromise: Promise<MediaStream>;
onMount(() => {
mediaStreamPromise = getMediaStream({ video: true }).then((mediaStream) => {
imageCapture = getImageCapture(mediaStream);
return mediaStream;
});
});
</script>
<div class="Container">
{#await mediaStreamPromise}
<h1>Waiting for camera....</h1>
{:then mediaStream}
<CameraPreview {mediaStream} {height} {width} />
{:catch error}
<h1>Error: {error.message}</h1>
{/await}
</div>
<style>
.Container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
</style>

View File

@ -0,0 +1,24 @@
<script lang="ts">
export let mediaStream: MediaStream;
export let width: number;
export let height: number;
function srcObject(node, stream) {
node.srcObject = stream;
return {
update(newStream) {
if (node.srcObject != newStream) {
node.srcObject = newStream;
}
},
};
}
</script>
<video
use:srcObject={mediaStream}
width={width}
height={height}
autoplay
playsinline
/>

View File

@ -4,18 +4,15 @@ enum Methods {
'PUT' = 'PUT',
}
export async function get<T>(route: string): Promise<T | null> {
export async function get<T>(route: string): Promise<T> {
return request<T>('GET', route);
}
export async function post<T>(
route: string,
data?: unknown,
): Promise<T | null> {
export async function post<T>(route: string, data?: unknown): Promise<T> {
return request<T>('POST', route, data);
}
export async function put<T>(route: string, data: unknown): Promise<T | null> {
export async function put<T>(route: string, data: unknown): Promise<T> {
return request<T>('PUT', route, data);
}
@ -23,7 +20,7 @@ async function request<T>(
method: keyof typeof Methods,
route: string,
data?: unknown,
): Promise<T | null> {
): Promise<T> {
const response = await fetch(`${process.env.APIURL}${route}`, {
headers: {
'content-type': 'application/json',
@ -34,8 +31,11 @@ async function request<T>(
body: data ? JSON.stringify(data) : null,
});
if (!response.ok) throw new Error(await response.json());
if (response.ok && response.status !== 204) {
return response.json() as T;
}
return null;
return {} as T;
}

View File

@ -1,22 +1,18 @@
import type { CreateUserBody, LoginUserBody, UserInfo } from '@core';
import { post, get } from './request';
import { currentUser } from '../store/user';
export async function login(raw: LoginUserBody) {
const user = await post<UserInfo>('/user/login', raw);
currentUser.set(user);
return user;
return post<UserInfo>('/user/login', raw);
}
export async function logout() {
await post('/user/logout');
currentUser.set(null);
await post<void>('/user/logout');
}
export async function read(uuid: string) {
const user = await get<UserInfo>(`/user/read/${uuid}`);
return get<UserInfo>(`/user/read/${uuid}`);
}
export async function create(raw: CreateUserBody) {
const user = await post<UserInfo>('/user/create', raw);
return post<UserInfo>('/user/create', raw);
}

View File

@ -0,0 +1,10 @@
function getMediaStream(constraints = { video: true }) {
return navigator.mediaDevices.getUserMedia(constraints);
}
function getImageCapture(mediaStream) {
const track = mediaStream.getVideoTracks()[0];
return new ImageCapture(track);
}
export { getMediaStream, getImageCapture };

View File

@ -3,6 +3,7 @@
import NavBar from '../components/NavBar.svelte';
import { Form, Input, Button } from 'spaper';
import { login } from '../functions/user';
import { currentUser } from '../store/user';
import { useNavigate } from 'svelte-navigator';
const user: LoginUserBody = {
@ -13,10 +14,11 @@
const navigate = useNavigate();
const loginHandler = async () => {
const result = await login(user);
if (result) {
try {
const res = await login(user);
currentUser.set(res);
navigate('/');
}
} catch (error) {}
};
</script>
@ -39,7 +41,7 @@
type="secondary"
class="row sm-2 col-4"
block
on:click={() => loginHandler()}>Sign in</Button
on:click={loginHandler}>Sign in</Button
>
</Form>
</div>

View File

@ -8,12 +8,16 @@
const navigate = useNavigate();
const logoutHandler = async () => {
try {
await logout();
} finally {
currentUser.set(null);
navigate('/');
}
};
</script>
<Button type="danger" block on:click={() => logoutHandler()}>Logout</Button>
<Button type="danger" block on:click={logoutHandler}>Logout</Button>
{$currentUser?.uuid}
{$currentUser?.username}
{$currentUser?.role}

View File

@ -0,0 +1,26 @@
<script lang="ts">
import View3D from '../components/3DHandler.svelte';
import Camera from '../components/CameraHandler.svelte';
import NavBar from '../components/NavBar.svelte';
let containerHeight: number;
let containerWidth: number;
</script>
<div
class="viewAR"
bind:clientHeight={containerHeight}
bind:clientWidth={containerWidth}
>
<Camera height={containerHeight} width={containerWidth} />
<View3D height={containerHeight} width={containerWidth} />
</div>
<NavBar />
<style>
.viewAR {
position: absolute;
height: 100%;
width: 100%;
}
</style>