xiongzhu %!s(int64=2) %!d(string=hai) anos
pai
achega
1bd82368fb

+ 2 - 2
.env

@@ -21,5 +21,5 @@ OSS_REGION=oss-cn-hangzhou
 OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
 GOOGLE_CLIENT_ID=329299284157-vap0i4k5bngiq97jfrskbt4972d3775s.apps.googleusercontent.com
 GOOGLE_CLIENT_SECRET=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
-FACEBOOK_CLIENT_ID=clientId
-FACEBOOK_CLIENT_SECRET=clientSecret
+FACEBOOK_CLIENT_ID=1373239366627803
+FACEBOOK_CLIENT_SECRET=357318ad9e6c85507424e6ab0b281ebf

+ 6 - 3
app/Controllers/Http/EpisodesController.ts

@@ -12,7 +12,8 @@ export default class EpisodesController {
         return await this.paginationService.paginate(request.all())
     }
 
-    public async store({ request }: HttpContextContract) {
+    public async store({ request, bouncer }: HttpContextContract) {
+        await bouncer.authorize('admin')
         const data: any = await request.validate({
             schema: schema.create({
                 seriesId: schema.number(),
@@ -60,7 +61,8 @@ export default class EpisodesController {
         return episode
     }
 
-    public async update({ request, params }: HttpContextContract) {
+    public async update({ request, params, bouncer }: HttpContextContract) {
+        await bouncer.authorize('admin')
         const data: any = await request.validate({
             schema: schema.create({
                 seriesId: schema.number.optional(),
@@ -81,7 +83,8 @@ export default class EpisodesController {
         return await episode.save()
     }
 
-    public async destroy({ params }: HttpContextContract) {
+    public async destroy({ params, bouncer }: HttpContextContract) {
+        await bouncer.authorize('admin')
         const episode = await Episode.findOrFail(params.id)
         await episode.delete()
     }

+ 2 - 1
app/Controllers/Http/OrdersController.ts

@@ -49,7 +49,8 @@ export default class OrdersController {
             amount: data.price.negated(),
             type: data.type,
             seriesId: data.seriesId,
-            episodeId: data.episodeId
+            episodeId: data.episodeId,
+            episodeNum: data.episodeNum
         })
 
         return await Order.create(data)

+ 63 - 0
app/Controllers/Http/PlayHistoriesController.ts

@@ -0,0 +1,63 @@
+import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
+import PlayHistory from 'App/Models/PlayHistory'
+import PaginationService from 'App/Services/PaginationService'
+import { schema } from '@ioc:Adonis/Core/Validator'
+import Series from 'App/Models/Series'
+import Episode from 'App/Models/Episode'
+
+export default class PlayHistoriesController {
+    private paginationService = new PaginationService(PlayHistory)
+
+    public async index({ request, auth }: HttpContextContract) {
+        return await this.paginationService.paginate({
+            ...request.all(),
+            userId: auth.user!.id,
+            order: 'updatedAt,desc'
+        })
+    }
+
+    public async store({ request, auth }: HttpContextContract) {
+        const data: any = await request.validate({
+            schema: schema.create({
+                episodeId: schema.number(),
+                duration: schema.number()
+            })
+        })
+
+        const episode = await Episode.findOrFail(data.episodeId)
+        const series = await Series.findOrFail(episode.seriesId)
+        data.seriesId = series.id
+        data.userId = auth.user!.id
+
+        let history = await PlayHistory.query()
+            .where('userId', auth.user!.id)
+            .where('seriesId', data.seriesId)
+            .first()
+        if (history) {
+            history.episodeId = data.episodeId
+            history.duration = data.duration
+            await history.save()
+        } else {
+            history = await PlayHistory.create({
+                ...data,
+                userId: auth.user!.id
+            })
+        }
+
+        await Series.query().where('id', series.id).increment('playCount', 1)
+        await Episode.query().where('id', episode.id).increment('playCount', 1)
+
+        return history
+    }
+
+    public async show({ params }: HttpContextContract) {
+        return await PlayHistory.findOrFail(params.id)
+    }
+
+    public async destroy({ params, bouncer }: HttpContextContract) {
+        const playHistory = await PlayHistory.findOrFail(params.id)
+        await bouncer.authorize('owner', playHistory)
+        await playHistory.delete()
+        return playHistory
+    }
+}

+ 39 - 3
app/Controllers/Http/SeriesController.ts

@@ -7,12 +7,36 @@ import Decimal from 'decimal.js'
 export default class SeriesController {
     private paginationService = new PaginationService(Series)
     public async index({ request }: HttpContextContract) {
+        const data = request.all()
         return await this.paginationService.paginate(request.all(), (q) => {
             q.preload('categories')
+            this.queryCategories(q, data.categories)
         })
     }
 
-    public async store({ request }: HttpContextContract) {
+    private queryCategories(q: any, categories: any) {
+        if (categories) {
+            if (categories instanceof Array) {
+                categories.forEach((item, index) => {
+                    if (index === 0)
+                        q.whereHas('categories', (wh) => {
+                            wh.where('categories.id', item)
+                        })
+                    else
+                        q.orWhereHas('categories', (wh) => {
+                            wh.where('categories.id', item)
+                        })
+                })
+            } else {
+                q.whereHas('categories', (wh) => {
+                    wh.where('categories.id', categories)
+                })
+            }
+        }
+    }
+
+    public async store({ request, bouncer }: HttpContextContract) {
+        await bouncer.authorize('admin')
         await request.validate({
             schema: schema.create({
                 title: schema.string(),
@@ -32,7 +56,8 @@ export default class SeriesController {
         return data
     }
 
-    public async update({ params, request }: HttpContextContract) {
+    public async update({ params, request, bouncer }: HttpContextContract) {
+        await bouncer.authorize('admin')
         const serie = await Series.findOrFail(params.id)
         const payload = (await request.validate({
             schema: schema.create({
@@ -58,8 +83,19 @@ export default class SeriesController {
         return await serie.save()
     }
 
-    public async destroy({ params }: HttpContextContract) {
+    public async destroy({ params, bouncer }: HttpContextContract) {
+        await bouncer.authorize('admin')
         const serie = await Series.findOrFail(params.id)
         await serie.delete()
     }
+
+    public async search({ request }: HttpContextContract) {
+        const data = request.all()
+        return await this.paginationService.paginate(data, (q) => {
+            if (data.title) {
+                q.where('title', 'like', `%${data.title}%`)
+            }
+            this.queryCategories(q, data.categories)
+        })
+    }
 }

+ 3 - 0
app/Models/BalanceRecord.ts

@@ -37,4 +37,7 @@ export default class BalanceRecord extends AppBaseModel {
 
     @column()
     public episodeId?: number
+
+    @column()
+    public episodeNum?: number
 }

+ 31 - 0
app/Models/PlayHistory.ts

@@ -0,0 +1,31 @@
+import { DateTime } from 'luxon'
+import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'
+import Episode from './Episode'
+import Series from './Series'
+
+export default class PlayHistory extends BaseModel {
+    @column({ isPrimary: true })
+    public id: number
+
+    @column.dateTime({ autoCreate: true })
+    public createdAt: DateTime
+
+    @column.dateTime({ autoCreate: true, autoUpdate: true })
+    public updatedAt: DateTime
+
+    @column()
+    public userId: number
+
+    @column()
+    public seriesId: number
+
+    @column()
+    public episodeId: number
+
+    @column()
+    public duration: number
+
+    public series: Series
+
+    public episode: Episode
+}

+ 1 - 1
app/Models/Series.ts

@@ -4,7 +4,7 @@ import { stringArrayConverter, jsonConverter, decimalConverter } from 'App/Helpe
 import Decimal from 'decimal.js'
 import AppBaseModel from './AppBaseModel'
 import Category from './Category'
-export default class Serie extends AppBaseModel {
+export default class Series extends AppBaseModel {
     @column({ isPrimary: true })
     public id: number
 

+ 1 - 1
app/Services/PaginationService.ts

@@ -15,7 +15,7 @@ export default class PaginationService<T extends LucidModel> {
         }
         Object.keys(query).forEach((key) => {
             if (
-                ['page', 'pageSize', 'orderBy', 'preload'].includes(key) ||
+                ['page', 'pageSize', 'order', 'preload'].includes(key) ||
                 query[key] === undefined ||
                 query[key] === null ||
                 query[key] === ''

+ 1 - 0
app/Services/UserBalanceService.ts

@@ -22,6 +22,7 @@ class UserBalanceService {
         description?: string
         seriesId?: number
         episodeId?: number
+        episodeNum?: number
     }) {
         const userBalance = await this.getBalance(params.userId)
         userBalance.lastBalance = userBalance.balance

+ 2 - 2
config/ally.ts

@@ -27,7 +27,7 @@ const allyConfig: AllyConfig = {
         driver: 'google',
         clientId: Env.get('GOOGLE_CLIENT_ID'),
         clientSecret: Env.get('GOOGLE_CLIENT_SECRET'),
-        callbackUrl: 'http://localhost:3333/google/callback'
+        callbackUrl: 'http://localhost:3333/api/auth/google/callback'
     },
     /*
     |--------------------------------------------------------------------------
@@ -38,7 +38,7 @@ const allyConfig: AllyConfig = {
         driver: 'facebook',
         clientId: Env.get('FACEBOOK_CLIENT_ID'),
         clientSecret: Env.get('FACEBOOK_CLIENT_SECRET'),
-        callbackUrl: 'http://localhost:3333/facebook/callback'
+        callbackUrl: 'http://localhost:3333/api/auth/facebook/callback'
     }
 }
 

+ 11 - 0
database/migrations/1701400528329_balance_records.ts

@@ -0,0 +1,11 @@
+import BaseSchema from '@ioc:Adonis/Lucid/Schema'
+
+export default class extends BaseSchema {
+    protected tableName = 'balance_records'
+
+    public async up() {
+        this.schema.alterTable(this.tableName, (table) => {
+            table.integer('episode_num').nullable()
+        })
+    }
+}

+ 22 - 0
database/migrations/1701401566617_play_histories.ts

@@ -0,0 +1,22 @@
+import BaseSchema from '@ioc:Adonis/Lucid/Schema'
+
+export default class extends BaseSchema {
+    protected tableName = 'play_histories'
+
+    public async up() {
+        this.schema.createTable(this.tableName, (table) => {
+            table.increments('id')
+            table.datetime('created_at', { useTz: true })
+            table.datetime('updated_at', { useTz: true })
+            table.integer('user_id').unsigned().references('users.id').onDelete('CASCADE')
+            table.integer('series_id').unsigned().references('series.id').onDelete('CASCADE')
+            table.integer('episode_id').unsigned().references('episodes.id').onDelete('CASCADE')
+            table.integer('duration').unsigned()
+            table.unique(['user_id', 'series_id'])
+        })
+    }
+
+    public async down() {
+        this.schema.dropTable(this.tableName)
+    }
+}

+ 52 - 41
start/routes.ts

@@ -25,57 +25,68 @@ Route.group(() => {
         Route.post('login', 'AuthController.login')
         Route.post('register', 'AuthController.register')
         Route.post('admin/login', 'AuthController.loginAdmin')
-    }).prefix('/auth')
+        Route.get('/google/redirect', async ({ ally }) => {
+            return ally.use('google').redirect()
+        })
+        Route.get('/facebook/redirect', async ({ ally }) => {
+            return ally.use('facebook').redirect()
+        })
+        Route.get('/google/callback', async ({ ally }) => {
+            const google = ally.use('google')
 
-    Route.group(() => {
-        Route.post('upload', 'FilesController.store')
-        Route.get('sts', 'FilesController.sts')
-    })
-        .prefix('/files')
-        .middleware('auth:api')
+            /**
+             * User has explicitly denied the login request
+             */
+            if (google.accessDenied()) {
+                return 'Access was denied'
+            }
 
-    Route.group(() => {
-        Route.get('my', 'UsersController.my')
-        Route.get('admin/my', 'UsersController.myAdmin')
-    })
-        .prefix('users')
-        .middleware('auth:api')
+            /**
+             * Unable to verify the CSRF state
+             */
+            if (google.stateMisMatch()) {
+                return 'Request expired. Retry again'
+            }
 
-    Route.group(() => {
-        Route.resource('users', 'UsersController').apiOnly()
-    })
+            /**
+             * There was an unknown error during the redirect
+             */
+            if (google.hasError()) {
+                return google.getError()
+            }
 
-    Route.group(() => {
-        Route.resource('series', 'SeriesController').apiOnly()
-    })
+            /**
+             * Finally, access the user
+             */
+            const user = await google.user()
+            return user
+        })
+    }).prefix('/auth')
 
     Route.group(() => {
-        Route.resource('episodes', 'EpisodesController').apiOnly()
-    })
+        Route.get('search', 'SeriesController.search')
+    }).prefix('/series')
+    Route.resource('series', 'SeriesController').apiOnly()
+    Route.resource('episodes', 'EpisodesController').apiOnly()
+    Route.resource('categories', 'CategoriesController').apiOnly()
 
     Route.group(() => {
+        Route.group(() => {
+            Route.post('upload', 'FilesController.store')
+            Route.get('sts', 'FilesController.sts')
+        }).prefix('/files')
+        Route.resource('users', 'UsersController')
+        Route.group(() => {
+            Route.get('my', 'UsersController.my')
+            Route.get('admin/my', 'UsersController.myAdmin')
+        }).prefix('users')
         Route.resource('userBalances', 'UserBalancesController').apiOnly()
-    })
-
-    Route.group(() => {
-        Route.get('balanceRecords', 'BalanceRecordsController.index')
-    })
-
-    Route.group(() => {
+        Route.resource('balanceRecords', 'BalanceRecordsController').apiOnly()
         Route.resource('orders', 'OrdersController').apiOnly()
-    }).middleware('auth:api')
-
-    Route.group(() => {
-        Route.resource('categories', 'CategoriesController').only(['index', 'show'])
-        Route.resource('categories', 'CategoriesController')
-            .apiOnly()
-            .except(['index', 'show'])
-            .middleware({
-                '*': 'auth:api'
-            })
-    })
-
-    Route.group(() => {
         Route.resource('collections', 'CollectionsController').apiOnly()
+        Route.resource('playHistories', 'PlayHistoriesController').apiOnly()
+        Route.group(() => {
+            Route.get('report', 'PlayHistoriesController.report')
+        }).prefix('playHistories')
     }).middleware('auth:api')
 }).prefix('/api')