SQLite cokebank feature complete, bugfix time
[tpg/opendispense2.git] / src / cokebank_sqlite / main.c
1 /*
2  * OpenDispense 2 
3  * UCC (University [of WA] Computer Club) Electronic Accounting System
4  *
5  * SQLite Coke Bank (Accounts Database)
6  *
7  * This file is licenced under the 3-clause BSD Licence. See the file
8  * COPYING for full details.
9  */
10 #include <stdlib.h>
11 #include <limits.h>
12 #include <stdio.h>
13 #include <string.h>
14 #include "../cokebank.h"
15 #include <sqlite3.h>
16
17 const char * const csBank_DatabaseSetup = 
18 "CREATE TABLE IF NOT EXISTS accounts ("
19 "       acct_id INTEGER PRIMARY KEY NOT NULL,"
20 "       acct_balance INTEGER NOT NULL DEFAULT 0,"
21 "       acct_last_seen DATETIME NOT NULL DEFAULT (datetime('now')),"
22 "       acct_name STRING UNIQUE,"
23 "       acct_uid INTEGER UNIQUE DEFAULT NULL,"
24 "       acct_pin INTEGER DEFAULT NULL," //"     acct_pin INTEGER CHECK (acct_pin > 0 AND acct_pin < 10000) DEFAULT NULL,"
25 "       acct_is_disabled BOOLEAN NOT NULL DEFAULT false,"
26 "       acct_is_coke BOOLEAN NOT NULL DEFAULT false,"
27 "       acct_is_admin BOOLEAN NOT NULL DEFAULT false,"
28 "       acct_is_door BOOLEAN NOT NULL DEFAULT false,"
29 "       acct_is_internal BOOLEAN NOT NULL DEFAULT false"
30 ");"
31 "CREATE TABLE IF NOT EXISTS cards ("
32 "       acct_id INTEGER NOT NULL,"
33 "       card_name STRING NOT NULL UNIQUE,"
34 "       FOREIGN KEY (acct_id) REFERENCES accounts (acct_id) ON DELETE CASCADE"
35 //                       Deletion of the account frees the card  ^ ^ ^
36 ");"
37 "INSERT INTO accounts (acct_name,acct_is_admin,acct_uid) VALUES ('root',1,0);"
38 "INSERT INTO accounts (acct_name,acct_is_internal,acct_uid) VALUES ('"COKEBANK_SALES_ACCT"',1,-1);"
39 "INSERT INTO accounts (acct_name,acct_is_internal,acct_uid) VALUES ('"COKEBANK_DEBT_ACCT"',1,-2);"
40 "INSERT INTO accounts (acct_name,acct_is_internal,acct_uid) VALUES ('"COKEBANK_FREE_ACCT"',1,-3);"
41 ;
42
43 // === TYPES ===
44 struct sAcctIterator    // Unused really, just used as a void type
45 {
46 };
47
48 // === PROTOYPES ===
49  int    Bank_Initialise(const char *Argument);
50  int    Bank_Transfer(int SourceAcct, int DestAcct, int Ammount, const char *Reason);
51  int    Bank_GetFlags(int AcctID);
52  int    Bank_SetFlags(int AcctID, int Mask, int Value);
53  int    Bank_GetBalance(int AcctID);
54 char    *Bank_GetAcctName(int AcctID);
55 sqlite3_stmt    *Bank_int_MakeStatemnt(sqlite3 *Database, const char *Query);
56 sqlite3_stmt    *Bank_int_QuerySingle(sqlite3 *Database, const char *Query);
57  int    Bank_int_IsValidName(const char *Name);
58
59 // === GLOBALS ===
60 sqlite3 *gBank_Database;
61
62 // === CODE ===
63 int Bank_Initialise(const char *Argument)
64 {
65          int    rv;
66         char    *errmsg;
67         // Open database
68         rv = sqlite3_open(Argument, &gBank_Database);
69         if(rv != 0)
70         {
71                 fprintf(stderr, "CokeBank: Unable to open database '%s'\n", Argument);
72                 fprintf(stderr, "Reason: %s\n", sqlite3_errmsg(gBank_Database));
73                 sqlite3_close(gBank_Database);
74                 return 1;
75         }
76
77         // Check structure
78         rv = sqlite3_exec(gBank_Database, "SELECT acct_id FROM accounts LIMIT 1", NULL, NULL, &errmsg);
79         if( rv == SQLITE_OK )
80         {
81                 // NOP
82         }
83         else if( rv == SQLITE_NOTFOUND || rv == SQLITE_ERROR )
84         {
85                 sqlite3_free(errmsg);
86                 // Create tables
87                 rv = sqlite3_exec(gBank_Database, csBank_DatabaseSetup, NULL, NULL, &errmsg);
88                 if( rv != SQLITE_OK ) {
89                         fprintf(stderr, "Bank_Initialise - SQLite Error: %s\n", errmsg);
90                         sqlite3_free(errmsg);
91                         return 1;
92                 }
93         }
94         else
95         {
96                 // Unknown error
97                 fprintf(stderr, "Bank_Initialise - SQLite Error: %s (rv = %i)\n", errmsg, rv);
98                 sqlite3_free(errmsg);
99                 return 1;
100         }
101
102         return 0;
103 }
104
105 /*
106  * Move Money
107  */
108 int Bank_Transfer(int SourceUser, int DestUser, int Ammount, const char *Reason)
109 {
110         char    *query;
111          int    rv;
112         char    *errmsg;
113         
114         Reason = "";    // Shut GCC up
115         
116         // Begin SQL Transaction
117         sqlite3_exec(gBank_Database, "BEGIN TRANSACTION", NULL, NULL, NULL);
118
119         // Take from the source
120         query = mkstr("UPDATE accounts SET acct_balance=acct_balance-%i,acct_last_seen=datetime('now') WHERE acct_id=%i", Ammount, SourceUser);
121         printf("query = \"%s\"\n", query);
122         rv = sqlite3_exec(gBank_Database, query, NULL, NULL, &errmsg);
123         free(query);
124         if( rv != SQLITE_OK )
125         {
126                 fprintf(stderr, "Bank_Transfer - SQLite Error: %s\n", errmsg);
127                 sqlite3_free(errmsg);
128                 sqlite3_exec(gBank_Database, "ROLLBACK", NULL, NULL, NULL);
129                 return 1;
130         }
131
132         // Give to the destination
133         query = mkstr("UPDATE accounts SET acct_balance=acct_balance+%i,acct_last_seen=datetime('now') WHERE acct_id=%i", Ammount, DestUser);
134         printf("query = \"%s\"\n", query);
135         rv = sqlite3_exec(gBank_Database, query, NULL, NULL, &errmsg);
136         free(query);
137         if( rv != SQLITE_OK )
138         {
139                 fprintf(stderr, "Bank_Transfer - SQLite Error: %s\n", errmsg);
140                 sqlite3_free(errmsg);
141                 sqlite3_exec(gBank_Database, "ROLLBACK", NULL, NULL, NULL);
142                 return 1;
143         }
144
145         // Commit transaction
146         sqlite3_exec(gBank_Database, "COMMIT", NULL, NULL, NULL);
147
148         return 0;
149 }
150
151 /*
152  * Get user flags
153  */
154 int Bank_GetFlags(int UserID)
155 {
156         sqlite3_stmt    *statement;
157         char    *query;
158          int    ret;
159
160         // Build Query
161         query = mkstr(
162                 "SELECT acct_is_disabled,acct_is_coke,acct_is_wheel,acct_is_door,acct_is_internal"
163                 " FROM accounts WHERE acct_id=%i LIMIT 1",
164                 UserID
165                 );
166         statement = Bank_int_QuerySingle(gBank_Database, query);
167         free(query);
168         if( !statement )        return -1;
169
170         // Get Flags
171         ret = 0;
172         // - Disabled
173         if( sqlite3_column_int(statement, 0) )  ret |= USER_FLAG_DISABLED;
174         // - Coke
175         if( sqlite3_column_int(statement, 1) )  ret |= USER_FLAG_COKE;
176         // - Wheel
177         if( sqlite3_column_int(statement, 2) )  ret |= USER_FLAG_ADMIN;
178         // - Door
179         if( sqlite3_column_int(statement, 3) )  ret |= USER_FLAG_DOORGROUP;
180         // - Internal
181         if( sqlite3_column_int(statement, 4) )  ret |= USER_FLAG_INTERNAL;
182         
183         // Destroy and return
184         sqlite3_finalize(statement);
185         
186         return ret;
187 }
188
189 /*
190  * Set user flags
191  */
192 int Bank_SetFlags(int UserID, int Mask, int Value)
193 {
194         char    *query;
195          int    rv;
196         char    *errmsg;
197
198         #define MAP_FLAG(name, flag)    (Mask&(flag)?(Value&(flag)?","name"=1":","name"=0"):"")
199         query = mkstr(
200                 "UPDATE accounts SET acct_id=acct_id%s%s%s%s%s WHERE acct_id=%i",// LIMIT 1",
201                 MAP_FLAG("acct_is_coke", USER_FLAG_COKE),
202                 MAP_FLAG("acct_is_admin", USER_FLAG_ADMIN),
203                 MAP_FLAG("acct_is_door", USER_FLAG_DOORGROUP),
204                 MAP_FLAG("acct_is_internal", USER_FLAG_INTERNAL),
205                 MAP_FLAG("acct_is_disabled", USER_FLAG_DISABLED),
206                 UserID
207                 );
208         #undef MAP_FLAG
209
210         // Execute Query
211         rv = sqlite3_exec(gBank_Database, query, NULL, NULL, &errmsg);
212         if( rv != SQLITE_OK )
213         {
214                 fprintf(stderr, "Bank_SetUserFlags - SQLite Error: %s\n", errmsg);
215                 fprintf(stderr, "query = '%s'\n", query);
216                 free(query);
217                 sqlite3_free(errmsg);
218                 return -1;
219         }
220         free(query);
221         
222         return 0;
223 }
224
225 /*
226  * Get user balance
227  */
228 int Bank_GetBalance(int AcctID)
229 {
230         sqlite3_stmt    *statement;
231         char    *query;
232          int    ret;
233         
234         query = mkstr("SELECT acct_balance FROM accounts WHERE acct_id=%i LIMIT 1", AcctID);
235         statement = Bank_int_QuerySingle(gBank_Database, query);
236         free(query);
237         if( !statement )        return INT_MIN;
238         
239         // Read return value
240         ret = sqlite3_column_int(statement, 0);
241         
242         // Clean up and return
243         sqlite3_finalize(statement);
244         return ret;
245 }
246
247 /*
248  * Get the name of an account
249  */
250 char *Bank_GetAcctName(int AcctID)
251 {
252         sqlite3_stmt    *statement;
253         char    *query;
254         char    *ret;
255         
256         query = mkstr("SELECT acct_name FROM accounts WHERE acct_id=%i LIMIT 1", AcctID);
257         statement = Bank_int_QuerySingle(gBank_Database, query);
258         free(query);
259         if( !statement )        return NULL;
260         
261         // Read return value
262         ret = strdup( (const char*)sqlite3_column_text(statement, 0) );
263         
264         // Clean up and return
265         sqlite3_finalize(statement);
266         return ret;
267 }
268
269 /*
270  * Get an account ID from a name
271  */
272 int Bank_GetAcctByName(const char *Name)
273 {
274         char    *query;
275         sqlite3_stmt    *statement;
276          int    ret;
277         
278         if( !Bank_int_IsValidName(Name) )       return -1;
279         
280         query = mkstr("SELECT acct_id FROM accounts WHERE acct_name='%s' LIMIT 1", Name);
281         statement = Bank_int_QuerySingle(gBank_Database, query);
282         free(query);
283         if( !statement )        return -1;
284         
285         ret = sqlite3_column_int(statement, 0);
286         if( ret == 0 )  return -1;
287         
288         sqlite3_finalize(statement);
289         return ret;
290 }
291
292 /*
293  * Create a new named account
294  */
295 int Bank_CreateAcct(const char *Name)
296 {
297         char    *query;
298         char    *errmsg;
299          int    rv;
300         
301         if( Name )
302         {
303                 if( !Bank_int_IsValidName(Name) )       return -1;
304                 query = mkstr("INSERT INTO accounts (acct_name) VALUES ('%s')", Name);
305         }
306         else
307         {
308                 query = strdup("INSERT INTO accounts (acct_name) VALUES (NULL)");
309         }
310                 
311         rv = sqlite3_exec(gBank_Database, query, NULL, NULL, &errmsg);
312         if( rv != SQLITE_OK )
313         {
314                 fprintf(stderr, "Bank_CreateAcct - SQLite Error: '%s'\n", errmsg);
315                 fprintf(stderr, "Query = '%s'\n", query);
316                 sqlite3_free(errmsg);
317                 free(query);
318                 return -1;
319         }
320         
321         free(query);
322         
323         return sqlite3_last_insert_rowid(gBank_Database);
324 }
325
326 /*
327  * Create an iterator for user accounts
328  */
329 tAcctIterator *Bank_Iterator(int FlagMask, int FlagValues, int Flags, int MinMaxBalance, time_t LastSeen)
330 {
331         char    *query;
332         const char      *balanceClause;
333         const char      *lastSeenClause;
334         const char      *orderClause;
335         const char      *revSort;
336         sqlite3_stmt    *ret;
337         
338         if( Flags & BANK_ITFLAG_MINBALANCE )
339                 balanceClause = " AND acct_balance>=";
340         else if( Flags & BANK_ITFLAG_MAXBALANCE )
341                 balanceClause = " AND acct_balance<=";
342         else {
343                 balanceClause = " AND 1!=";
344                 MinMaxBalance = 0;
345         }
346         
347         if( Flags & BANK_ITFLAG_SEENAFTER )
348                 lastSeenClause = " AND acct_last_seen>=";
349         else if( Flags & BANK_ITFLAG_SEENBEFORE )
350                 lastSeenClause = " AND acct_last_seen<=";
351         else {
352                 lastSeenClause = " AND datetime(-1,'unixepoch')!=";
353         }
354         
355         switch( Flags & BANK_ITFLAG_SORTMASK )
356         {
357         case BANK_ITFLAG_SORT_NONE:
358                 orderClause = "";
359                 revSort = "";
360                 break;
361         case BANK_ITFLAG_SORT_NAME:
362                 orderClause = "ORDER BY acct_name";
363                 revSort = " DESC";
364                 break;
365         case BANK_ITFLAG_SORT_BAL:
366                 orderClause = "ORDER BY acct_balance";
367                 revSort = " DESC";
368                 break;
369         case BANK_ITFLAG_SORT_LASTSEEN:
370                 orderClause = "ORDER BY acct_balance";
371                 revSort = " DESC";
372                 break;
373         default:
374                 fprintf(stderr, "BUG: Unknown sort (%x) in SQLite CokeBank\n", Flags & BANK_ITFLAG_SORTMASK);
375                 return NULL;
376         }
377         if( !(Flags & BANK_ITFLAG_REVSORT) )
378                 revSort = "";
379         
380         #define MAP_FLAG(name, flag)    (FlagMask&(flag)?(FlagValues&(flag)?" AND "name"=1":" AND "name"=0"):"")
381         query = mkstr("SELECT acct_id FROM accounts WHERE 1=1"
382                 "%s%s%s%s%s"    // Flags
383                 "%s%i"  // Balance
384                 "%sdatetime(%lli,'unixepoch')"  // Last seen
385                 "%s%s"  // Sort and direction
386                 ,
387                 MAP_FLAG("acct_is_coke", USER_FLAG_COKE),
388                 MAP_FLAG("acct_is_admin", USER_FLAG_ADMIN),
389                 MAP_FLAG("acct_is_door", USER_FLAG_DOORGROUP),
390                 MAP_FLAG("acct_is_internal", USER_FLAG_INTERNAL),
391                 MAP_FLAG("acct_is_disabled", USER_FLAG_DISABLED),
392                 balanceClause, MinMaxBalance,
393                 lastSeenClause, LastSeen,
394                 orderClause, revSort
395                 );
396         //printf("query = \"%s\"\n", query);
397         #undef MAP_FLAG
398         
399         ret = Bank_int_MakeStatemnt(gBank_Database, query);
400         if( !ret )      return NULL;
401         
402         free(query);
403         
404         return (void*)ret;
405 }
406
407 /*
408  * Get the next account in an iterator
409  */
410 int Bank_IteratorNext(tAcctIterator *It)
411 {
412          int    rv;
413         rv = sqlite3_step( (sqlite3_stmt*)It );
414         
415         if( rv == SQLITE_DONE ) return -1;
416         if( rv != SQLITE_ROW ) {
417                 fprintf(stderr, "Bank_IteratorNext - SQLite Error: %s\n", sqlite3_errmsg(gBank_Database));
418                 return -1;
419         }
420         
421         return sqlite3_column_int( (sqlite3_stmt*)It, 0 );
422 }
423
424 /*
425  * Free an interator
426  */
427 void Bank_DelIterator(tAcctIterator *It)
428 {
429         sqlite3_finalize( (sqlite3_stmt*)It );
430 }
431
432 /*
433  * Check user authentication token
434  */
435 int Bank_GetUserAuth(const char *Salt, const char *Username, const char *Password)
436 {
437         Salt = Password = Username;     // Shut up GCC
438         // DEBUG HACKS!
439         #if 1
440         return Bank_GetAcctByName(Username);
441         #else
442         return -1;
443         #endif
444 }
445
446 /*
447  * Get an account number given a card ID
448  * NOTE: Actually ends up just being an alternate authentication token,
449  *       as no checking is done on the ID's validity, save for SQL sanity.
450  */
451 int Bank_GetAcctByCard(const char *CardID)
452 {
453         char    *query;
454         sqlite3_stmt    *statement;
455          int    ret;
456         
457         if( !Bank_int_IsValidName(CardID) )
458                 return -1;
459         
460         query = mkstr("SELECT acct_id FROM cards WHERE card_name='%s' LIMIT 1", CardID);
461         statement = Bank_int_QuerySingle(gBank_Database, query);
462         free(query);
463         if( !statement )        return -1;
464         
465         ret = sqlite3_column_int(statement, 0);
466         
467         sqlite3_finalize(statement);
468         
469         return ret;
470 }
471
472 /*
473  * Add a card to an account
474  */
475 int Bank_AddAcctCard(int AcctID, const char *CardID)
476 {
477         char    *query;
478          int    rv;
479         char    *errmsg;
480         
481         if( !Bank_int_IsValidName(CardID) )
482                 return -1;
483         
484         // TODO: Check the AcctID too
485         
486         // Insert card
487         query = mkstr("INSERT INTO cards (acct_id,card_name) VALUES (%i,'%s')",
488                 AcctID, CardID);
489         rv = sqlite3_exec(gBank_Database, query, NULL, NULL, &errmsg);
490         if( rv == SQLITE_CONSTRAINT )
491         {
492                 sqlite3_free(errmsg);
493                 free(query);
494                 return 2;       // Card in use
495         }
496         if( rv != SQLITE_OK )
497         {
498                 fprintf(stderr, "Bank_AddAcctCard - SQLite Error: '%s'\n", errmsg);
499                 fprintf(stderr, "Query = '%s'\n", query);
500                 sqlite3_free(errmsg);
501                 free(query);
502                 return -1;
503         }
504         free(query);
505         
506         return 0;
507 }
508
509 /*
510  * Create a SQLite Statement
511  */
512 sqlite3_stmt *Bank_int_MakeStatemnt(sqlite3 *Database, const char *Query)
513 {
514          int    rv;
515         sqlite3_stmt    *ret;
516         rv = sqlite3_prepare_v2(Database, Query, strlen(Query)+1, &ret, NULL);
517         if( rv != SQLITE_OK ) {
518                 fprintf(stderr, "SQLite Error: %s\n", sqlite3_errmsg(Database));
519                 fprintf(stderr, "query = \"%s\"\n", Query);
520                 return NULL;
521         }
522         
523         return ret;
524 }
525
526 /*
527  * Create a SQLite statement and query it for the first row
528  * Returns NULL if the the set is empty
529  */
530 sqlite3_stmt *Bank_int_QuerySingle(sqlite3 *Database, const char *Query)
531 {
532         sqlite3_stmt    *ret;
533          int    rv;
534         
535         // Prepare query
536         ret = Bank_int_MakeStatemnt(Database, Query);
537         if( !ret )      return NULL;
538         
539         // Get row
540         rv = sqlite3_step(ret);
541         // - Empty result set
542         if( rv == SQLITE_DONE ) return NULL;
543         // - Other error
544         if( rv != SQLITE_ROW ) {
545                 fprintf(stderr, "SQLite Error: %s\n", sqlite3_errmsg(gBank_Database));
546                 fprintf(stderr, "query = \"%s\"\n", Query);
547                 return NULL;
548         }
549         
550         return ret;
551 }
552
553 /**
554  * \brief Checks if the passed account name is valid
555  */
556 int Bank_int_IsValidName(const char *Name)
557 {
558         while(*Name)
559         {
560                 if( *Name == '\'' )     return 0;
561                 Name ++;
562         }
563         return 1;
564 }

UCC git Repository :: git.ucc.asn.au